mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Compare commits
7 Commits
3b793a72b8
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
964f4ae236 | ||
|
|
9146315001 | ||
|
|
ed095ad0a7 | ||
|
|
82a8776911 | ||
|
|
753e9c4721 | ||
|
|
b64094ed20 | ||
|
|
bc854c63f7 |
@@ -1,6 +1,7 @@
|
|||||||
KeePassDX(4.3.0)
|
KeePassDX(4.3.0)
|
||||||
* Manual change of app language #1884 #1990
|
* Manual change of app language #1884 #1990
|
||||||
* Fix autofill username detection #2276
|
* 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
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ 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
|
||||||
@@ -90,9 +91,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 +113,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,12 +142,14 @@ 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)
|
||||||
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyId, credentialIds $credentialIdList")
|
||||||
SearchHelper.checkAutoSearchInfo(
|
SearchHelper.checkAutoSearchInfo(
|
||||||
context = this,
|
context = this,
|
||||||
database = mDatabase,
|
database = mDatabase,
|
||||||
@@ -175,26 +183,36 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
},
|
},
|
||||||
onItemNotFound = { _ ->
|
onItemNotFound = { _ ->
|
||||||
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyId")
|
||||||
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
if (credentialIdList.isEmpty()) {
|
||||||
PasskeyLauncherActivity.getPendingIntent(
|
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
||||||
context = applicationContext,
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
specialMode = SpecialMode.SELECTION,
|
context = applicationContext,
|
||||||
searchInfo = searchInfo
|
specialMode = SpecialMode.SELECTION,
|
||||||
)?.let { pendingIntent ->
|
searchInfo = searchInfo
|
||||||
passkeyEntries.add(
|
)?.let { pendingIntent ->
|
||||||
PublicKeyCredentialEntry(
|
passkeyEntries.add(
|
||||||
context = applicationContext,
|
PublicKeyCredentialEntry(
|
||||||
username = getString(R.string.passkey_database_username),
|
context = applicationContext,
|
||||||
displayName = getString(R.string.passkey_selection_description),
|
username = getString(R.string.passkey_database_username),
|
||||||
icon = defaultIcon,
|
displayName = getString(R.string.passkey_selection_description),
|
||||||
pendingIntent = pendingIntent,
|
icon = defaultIcon,
|
||||||
beginGetPublicKeyCredentialOption = option,
|
pendingIntent = pendingIntent,
|
||||||
lastUsedTime = Instant.now(),
|
beginGetPublicKeyCredentialOption = option,
|
||||||
isAutoSelectAllowed = isAutoSelectAllowed
|
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")
|
||||||
@@ -287,10 +305,11 @@ 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)
|
||||||
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||||
SearchHelper.checkAutoSearchInfo(
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
|||||||
@@ -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,97 @@ 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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"))
|
||||||
|
// 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 == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -200,6 +201,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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -775,4 +775,5 @@
|
|||||||
<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>
|
||||||
</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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
* Manual change of app language #1884 #1990
|
* Manual change of app language #1884 #1990
|
||||||
* Fix autofill username detection #2276
|
* Fix autofill username detection #2276
|
||||||
|
* Fix Passkey in passwordless mode #2282
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
* Changement manuel de la langue de l'appli #1884 #1990
|
* Changement manuel de la langue de l'appli #1884 #1990
|
||||||
* Correction de la détection du nom d'utilisateur pour le remplissage auto #2276
|
* 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