fix: Refactiring JSON objects

This commit is contained in:
J-Jamet
2025-08-16 19:57:50 +02:00
parent c7741115ff
commit 4a1cee619c
23 changed files with 1094 additions and 520 deletions

View File

@@ -41,14 +41,12 @@ import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.TypeMode
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.OriginHelper
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.addAuthCode
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrieveNodeId
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationComponent
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyCreationRequestParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
import com.kunzisoft.keepass.database.ContextualDatabase
@@ -115,7 +113,6 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
PendingIntentHandler.setCreateCredentialResponse(
intent = responseIntent,
response = buildCreatePublicKeyCredentialResponse(
packageName = packageName,
publicKeyCredentialCreationParameters = it
)
)
@@ -183,7 +180,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
finish()
} ?: run {
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
setResult(Activity.RESULT_CANCELED)
setResult(RESULT_CANCELED)
finish()
}
}
@@ -244,7 +241,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
finish()
} ?: run {
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
setResult(Activity.RESULT_CANCELED)
setResult(RESULT_CANCELED)
finish()
}
}
@@ -254,14 +251,10 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
searchInfo: SearchInfo
) {
Log.d(TAG, "Launch passkey registration")
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)?.callingAppInfo?.let { callingAppInfo ->
retrievePasskeyCreationRequestParameters(
creationOptions = intent.retrievePasskeyCreationComponent(),
webOrigin = OriginHelper.getWebOrigin(callingAppInfo, assets),
apkSigningCertificate =
callingAppInfo
.signingInfo.apkContentsSigners
.getOrNull(0)?.toByteArray(),
intent = intent,
assetManager = assets,
packageName = packageName,
passkeyCreated = { passkey, publicKeyCredentialParameters ->
// Save the requested parameters
mPasskey = passkey
@@ -315,7 +308,6 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
}
)
}
}
companion object {
private val TAG = PasskeyLauncherActivity::class.java.name

View File

@@ -46,7 +46,8 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
import com.kunzisoft.keepass.credentialprovider.SpecialMode
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
import com.kunzisoft.keepass.credentialprovider.passkey.util.JsonHelper
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.database.DatabaseTaskProvider
import com.kunzisoft.keepass.database.helper.SearchHelper
@@ -117,13 +118,11 @@ class PasskeyProviderService : CredentialProviderService() {
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
val relyingPartyJson = JsonHelper
.parseJsonToRequestOptions(option.requestJson)
.relyingParty
val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId
val searchInfo = SearchInfo().apply {
relyingParty = relyingPartyJson
relyingParty = relyingPartyId
}
Log.d(TAG, "Build passkey search for relying party $relyingPartyJson")
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
SearchHelper.checkAutoSearchInfo(
context = this,
database = mDatabase,
@@ -154,7 +153,7 @@ class PasskeyProviderService : CredentialProviderService() {
}
},
onItemNotFound = { _ ->
Log.w(TAG, "No passkey found in the database with this relying party : $relyingPartyJson")
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")
PasskeyLauncherActivity.getPendingIntent(
context = applicationContext,
@@ -252,11 +251,11 @@ class PasskeyProviderService : CredentialProviderService() {
val accountName = mDatabase?.name ?: getString(R.string.passkey_locked_database_username)
val createEntries: MutableList<CreateEntry> = mutableListOf()
val relyingPartyId = PublicKeyCredentialCreationOptions(request.requestJson).relyingPartyEntity.id
val searchInfo = SearchInfo().apply {
relyingParty = JsonHelper
.parseJsonToCreateOptions(request.requestJson)
.relyingParty
relyingParty = relyingPartyId
}
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
SearchHelper.checkAutoSearchInfo(
context = this,
database = mDatabase,

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import androidx.credentials.exceptions.GetCredentialUnknownException
import com.kunzisoft.asymmetric.Signature
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import org.json.JSONObject
class AuthenticatorAssertionResponse(
private val requestOptions: PublicKeyCredentialRequestOptions,
private val userPresent: Boolean,
private val userVerified: Boolean,
private val backupEligibility: Boolean,
private val backupState: Boolean,
private var userHandle: String,
privateKey: String,
private val clientDataResponse: ClientDataResponse,
) : AuthenticatorResponse {
override var clientJson = JSONObject()
private var authenticatorData: ByteArray = AuthenticatorData.buildAuthenticatorData(
relyingPartyId = requestOptions.rpId.toByteArray(),
userPresent = userPresent,
userVerified = userVerified,
backupEligibility = backupEligibility,
backupState = backupState
)
private var signature: ByteArray = byteArrayOf()
init {
signature = Signature.sign(privateKey, dataToSign())
?: throw GetCredentialUnknownException("signing failed")
}
private fun dataToSign(): ByteArray {
return authenticatorData + clientDataResponse.hashData()
}
override fun json(): JSONObject {
// https://www.w3.org/TR/webauthn-3/#authdata-flags
return clientJson.apply {
put("clientDataJSON", clientDataResponse.buildResponse())
put("authenticatorData", b64Encode(authenticatorData))
put("signature", b64Encode(signature))
put("userHandle", userHandle)
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import org.json.JSONArray
import org.json.JSONObject
class AuthenticatorAttestationResponse(
private val requestOptions: PublicKeyCredentialCreationOptions,
private val credentialId: ByteArray,
private val credentialPublicKey: ByteArray,
private val userPresent: Boolean,
private val userVerified: Boolean,
private val backupEligibility: Boolean,
private val backupState: Boolean,
private val publicKeyTypeId: Long,
private val publicKeyCbor: ByteArray,
private val clientDataResponse: ClientDataResponse,
) : AuthenticatorResponse {
override var clientJson = JSONObject()
var attestationObject: ByteArray
init {
attestationObject = defaultAttestationObject()
}
private fun buildAuthData(): ByteArray {
return AuthenticatorData.buildAuthenticatorData(
relyingPartyId = requestOptions.relyingPartyEntity.id.toByteArray(),
userPresent = userPresent,
userVerified = userVerified,
backupEligibility = backupEligibility,
backupState = backupState,
attestedCredentialData = true
) + AAGUID +
//credIdLen
byteArrayOf((credentialId.size shr 8).toByte(), credentialId.size.toByte()) +
credentialId +
credentialPublicKey
}
internal fun defaultAttestationObject(): ByteArray {
val ao = mutableMapOf<String, Any>()
ao.put("fmt", "none")
ao.put("attStmt", emptyMap<Any, Any>())
ao.put("authData", buildAuthData())
return Cbor().encode(ao)
}
override fun json(): JSONObject {
// See AuthenticatorAttestationResponseJSON at
// https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
return clientJson.apply {
put("clientDataJSON", clientDataResponse.buildResponse())
put("authenticatorData", b64Encode(buildAuthData()))
put("transports", JSONArray(listOf("internal", "hybrid")))
put("publicKey", b64Encode(publicKeyCbor))
put("publicKeyAlgorithm", publicKeyTypeId)
put("attestationObject", b64Encode(attestationObject))
}
}
companion object {
// TODO Authenticator Attestation Global Unique Identifier
private val AAGUID = ByteArray(16) { 0 }
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.encrypt.HashManager
class AuthenticatorData {
companion object {
fun buildAuthenticatorData(
relyingPartyId: ByteArray,
userPresent: Boolean,
userVerified: Boolean,
backupEligibility: Boolean,
backupState: Boolean,
attestedCredentialData: Boolean = false
): ByteArray {
// https://www.w3.org/TR/webauthn-3/#table-authData
var flags = 0
if (userPresent)
flags = flags or 0x01
// bit at index 1 is reserved
if (userVerified)
flags = flags or 0x04
if (backupEligibility)
flags = flags or 0x08
if (backupState)
flags = flags or 0x10
// bit at index 5 is reserved
if (attestedCredentialData) {
flags = flags or 0x40
}
// bit at index 7: Extension data included == false
return HashManager.hashSha256(relyingPartyId) +
byteArrayOf(flags.toByte()) +
byteArrayOf(0, 0, 0, 0)
}
}
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import org.json.JSONObject
interface AuthenticatorResponse {
var clientJson: JSONObject
fun json(): JSONObject
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import androidx.annotation.RestrictTo
@RestrictTo(RestrictTo.Scope.LIBRARY)
class Cbor {
data class Item(val item: Any, val len: Int)
data class Arg(val arg: Long, val len: Int)
val TYPE_UNSIGNED_INT = 0x00
val TYPE_NEGATIVE_INT = 0x01
val TYPE_BYTE_STRING = 0x02
val TYPE_TEXT_STRING = 0x03
val TYPE_ARRAY = 0x04
val TYPE_MAP = 0x05
val TYPE_TAG = 0x06
val TYPE_FLOAT = 0x07
fun decode(data: ByteArray): Any {
val ret = parseItem(data, 0)
return ret.item
}
fun encode(data: Any): ByteArray {
if (data is Number) {
if (data is Double) {
throw IllegalArgumentException("Don't support doubles yet")
} else {
val value = data.toLong()
if (value >= 0) {
return createArg(TYPE_UNSIGNED_INT, value)
} else {
return createArg(TYPE_NEGATIVE_INT, -1 - value)
}
}
}
if (data is ByteArray) {
return createArg(TYPE_BYTE_STRING, data.size.toLong()) + data
}
if (data is String) {
return createArg(TYPE_TEXT_STRING, data.length.toLong()) + data.encodeToByteArray()
}
if (data is List<*>) {
var ret = createArg(TYPE_ARRAY, data.size.toLong())
for (i in data) {
ret += encode(i!!)
}
return ret
}
if (data is Map<*, *>) {
// See:
// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#ctap2-canonical-cbor-encoding-form
var ret = createArg(TYPE_MAP, data.size.toLong())
var byteMap: MutableMap<ByteArray, ByteArray> = mutableMapOf()
for (i in data) {
// Convert to byte arrays so we can sort them.
byteMap.put(encode(i.key!!), encode(i.value!!))
}
var keysList = ArrayList<ByteArray>(byteMap.keys)
keysList.sortedWith(
Comparator<ByteArray> { a, b ->
// If two keys have different lengths, the shorter one sorts earlier;
// If two keys have the same length, the one with the lower value in (byte-wise)
// lexical order sorts earlier.
var aBytes = byteMap.get(a)!!
var bBytes = byteMap.get(b)!!
when {
a.size > b.size -> 1
a.size < b.size -> -1
aBytes.size > bBytes.size -> 1
aBytes.size < bBytes.size -> -1
else -> 0
}
}
)
for (key in keysList) {
ret += key
ret += byteMap.get(key)!!
}
return ret
}
throw IllegalArgumentException("Bad type")
}
private fun getType(data: ByteArray, offset: Int): Int {
val d = data[offset].toInt()
return (d and 0xFF) shr 5
}
private fun getArg(data: ByteArray, offset: Int): Arg {
val arg = data[offset].toLong() and 0x1F
if (arg < 24) {
return Arg(arg, 1)
}
if (arg == 24L) {
return Arg(data[offset + 1].toLong() and 0xFF, 2)
}
if (arg == 25L) {
var ret = (data[offset + 1].toLong() and 0xFF) shl 8
ret = ret or (data[offset + 2].toLong() and 0xFF)
return Arg(ret, 3)
}
if (arg == 26L) {
var ret = (data[offset + 1].toLong() and 0xFF) shl 24
ret = ret or ((data[offset + 2].toLong() and 0xFF) shl 16)
ret = ret or ((data[offset + 3].toLong() and 0xFF) shl 8)
ret = ret or (data[offset + 4].toLong() and 0xFF)
return Arg(ret, 5)
}
throw IllegalArgumentException("Bad arg")
}
private fun parseItem(data: ByteArray, offset: Int): Item {
val itemType = getType(data, offset)
val arg = getArg(data, offset)
println("Type $itemType ${arg.arg} ${arg.len}")
when (itemType) {
TYPE_UNSIGNED_INT -> {
return Item(arg.arg, arg.len)
}
TYPE_NEGATIVE_INT -> {
return Item(-1 - arg.arg, arg.len)
}
TYPE_BYTE_STRING -> {
val ret =
data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
return Item(ret, arg.len + arg.arg.toInt())
}
TYPE_TEXT_STRING -> {
val ret =
data.sliceArray(offset + arg.len.toInt() until offset + arg.len.toInt() + arg.arg.toInt())
return Item(ret.toString(Charsets.UTF_8), arg.len + arg.arg.toInt())
}
TYPE_ARRAY -> {
val ret = mutableListOf<Any>()
var consumed = arg.len
for (i in 0 until arg.arg.toInt()) {
val item = parseItem(data, offset + consumed)
ret.add(item.item)
consumed += item.len
}
return Item(ret.toList(), consumed)
}
TYPE_MAP -> {
val ret = mutableMapOf<Any, Any>()
var consumed = arg.len
for (i in 0 until arg.arg.toInt()) {
val key = parseItem(data, offset + consumed)
consumed += key.len
val value = parseItem(data, offset + consumed)
consumed += value.len
ret[key.item] = value.item
}
return Item(ret.toMap(), consumed)
}
else -> {
throw IllegalArgumentException("Bad type")
}
}
}
private fun createArg(type: Int, arg: Long): ByteArray {
val t = type shl 5
val a = arg.toInt()
if (arg < 24) {
return byteArrayOf(((t or a) and 0xFF).toByte())
}
if (arg <= 0xFF) {
return byteArrayOf(((t or 24) and 0xFF).toByte(), (a and 0xFF).toByte())
}
if (arg <= 0xFFFF) {
return byteArrayOf(
((t or 25) and 0xFF).toByte(),
((a shr 8) and 0xFF).toByte(),
(a and 0xFF).toByte()
)
}
if (arg <= 0xFFFFFFFF) {
return byteArrayOf(
((t or 26) and 0xFF).toByte(),
((a shr 24) and 0xFF).toByte(),
((a shr 16) and 0xFF).toByte(),
((a shr 8) and 0xFF).toByte(),
(a and 0xFF).toByte()
)
}
throw IllegalArgumentException("bad Arg")
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
open class ClientDataDefinedResponse(
private val clientDataHash: ByteArray
): ClientDataResponse {
override fun hashData(): ByteArray {
return clientDataHash
}
override fun buildResponse(): String {
return CLIENT_DATA_JSON_PRIVILEGED
}
companion object {
private const val CLIENT_DATA_JSON_PRIVILEGED = "<placeholder>"
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import org.json.JSONObject
open class ClientDataNotDefinedResponse(
type: Type,
challenge: ByteArray,
origin: String,
crossOrigin: Boolean? = null,
topOrigin: String? = null,
packageName: String?
): AuthenticatorResponse, ClientDataResponse {
override var clientJson = JSONObject()
init {
// https://w3c.github.io/webauthn/#client-data
clientJson.put("type", type.value)
clientJson.put("challenge", b64Encode(challenge))
clientJson.put("origin", origin)
crossOrigin?.let {
clientJson.put("crossOrigin", it)
}
topOrigin?.let {
clientJson.put("topOrigin", it)
}
packageName?.let {
clientJson.put("androidPackageName", packageName)
}
}
override fun json(): JSONObject {
return clientJson
}
enum class Type(val value: String) {
GET("webauthn.get"), CREATE("webauthn.create")
}
override fun buildResponse(): String {
return b64Encode(json().toString().toByteArray())
}
override fun hashData(): ByteArray {
return HashManager.hashSha256(json().toString().toByteArray())
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
interface ClientDataResponse {
fun hashData(): ByteArray
fun buildResponse(): String
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
data class PublicKeyCredentialRpEntity(val name: String, val id: String)
data class PublicKeyCredentialUserEntity(
val name: String,
val id: ByteArray,
val displayName: String
)
data class PublicKeyCredentialParameters(val type: String, val alg: Long)
data class PublicKeyCredentialDescriptor(
val type: String,
val id: ByteArray,
val transports: List<String>
)
data class AuthenticatorSelectionCriteria(
val authenticatorAttachment: String,
val residentKey: String,
val requireResidentKey: Boolean = false,
val userVerification: String = "preferred"
)

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import org.json.JSONObject
class FidoPublicKeyCredential(
val id: String,
val response: AuthenticatorResponse,
val authenticatorAttachment: String
) {
fun json(): String {
// see at https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
val discoverableCredential = true
val rk = JSONObject()
rk.put("rk", discoverableCredential)
val credProps = JSONObject()
credProps.put("credProps", rk)
// See RegistrationResponseJSON at
// https://w3c.github.io/webauthn/#ref-for-dom-publickeycredential-tojson
val ret = JSONObject()
ret.put("id", id)
ret.put("rawId", id)
ret.put("type", "public-key")
ret.put("authenticatorAttachment", authenticatorAttachment)
ret.put("response", response.json())
ret.put("clientExtensionResults", JSONObject()) // TODO credProps
return ret.toString()
}
}

View File

@@ -1,9 +1,72 @@
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
data class PublicKeyCredentialCreationOptions(
val relyingParty: String,
val challenge: ByteArray, // TODO Equals Hashcode
val username: String,
val userId: ByteArray, // TODO Equals Hashcode
val keyTypeIdList: List<Long>
import android.util.Log
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper
import org.json.JSONObject
class PublicKeyCredentialCreationOptions(requestJson: String) {
val json: JSONObject = JSONObject(requestJson)
val relyingPartyEntity: PublicKeyCredentialRpEntity
val userEntity: PublicKeyCredentialUserEntity
val challenge: ByteArray
val pubKeyCredParams: List<PublicKeyCredentialParameters>
var timeout: Long
var excludeCredentials: List<PublicKeyCredentialDescriptor>
var authenticatorSelection: AuthenticatorSelectionCriteria
var attestation: String
init {
val rpJson = json.getJSONObject("rp")
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)
// TODO: Fix excludeCredentials and authenticatorSelection
excludeCredentials = emptyList()
authenticatorSelection = AuthenticatorSelectionCriteria("platform", "required")
attestation = json.optString("attestation", "none")
Log.i("WebAuthn", "Challenge $challenge()")
Log.i("WebAuthn", "rp $relyingPartyEntity")
Log.i("WebAuthn", "user $userEntity")
Log.i("WebAuthn", "pubKeyCredParams $pubKeyCredParams")
Log.i("WebAuthn", "timeout $timeout")
Log.i("WebAuthn", "excludeCredentials $excludeCredentials")
Log.i("WebAuthn", "authenticatorSelection $authenticatorSelection")
Log.i("WebAuthn", "attestation $attestation")
}
}

View File

@@ -1,11 +1,29 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
import java.security.KeyPair
data class PublicKeyCredentialCreationParameters(
val relyingParty: String,
val publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions,
val credentialId: ByteArray, // TODO Equals Hashcode
val signatureKey: Pair<KeyPair, Long>,
val isPrivilegedApp: Boolean,
val challenge: ByteArray, // TODO Equals Hashcode
val clientDataResponse: ClientDataResponse
)

View File

@@ -1,6 +1,31 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
data class PublicKeyCredentialRequestOptions(
val relyingParty: String,
val challengeString: String
)
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper
import org.json.JSONObject
class PublicKeyCredentialRequestOptions(requestJson: String) {
val json: JSONObject = JSONObject(requestJson)
val challenge: ByteArray = Base64Helper.b64Decode(json.getString("challenge"))
val timeout: Long = json.optLong("timeout", 0)
val rpId: String = json.optString("rpId", "")
val userVerification: String = json.optString("userVerification", "preferred")
}

View File

@@ -1,9 +1,25 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.data
data class PublicKeyCredentialUsageParameters(
val relyingParty: String,
val packageName: String? = null,
val clientDataHash: ByteArray?, // TODO Equals Hashcode
val isPrivilegedApp: Boolean,
val challenge: ByteArray, // TODO Equals Hashcode
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
val clientDataResponse: ClientDataResponse
)

View File

@@ -1,19 +1,41 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.util
import org.apache.commons.codec.binary.Base64
import android.util.Base64
class Base64Helper {
companion object {
fun b64Decode(encodedString: String?): ByteArray {
return Base64.decodeBase64(encodedString)
fun b64Decode(encodedString: String): ByteArray {
return Base64.decode(
encodedString,
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE
)
}
fun b64Encode(data: ByteArray): String {
return android.util.Base64.encodeToString(
return Base64.encodeToString(
data,
android.util.Base64.NO_PADDING or android.util.Base64.NO_WRAP or android.util.Base64.URL_SAFE
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE
)
}
}

View File

@@ -1,249 +0,0 @@
package com.kunzisoft.keepass.credentialprovider.passkey.util
import android.annotation.SuppressLint
import androidx.credentials.webauthn.Cbor
import com.kunzisoft.encrypt.HashManager
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Decode
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import org.json.JSONArray
import org.json.JSONObject
class JsonHelper {
companion object {
fun generateClientDataJsonNonPrivileged(
challenge: ByteArray,
origin: String,
packageName: String?,
isGet: Boolean,
isCrossOriginAdded: Boolean
): String {
val clientJson = JSONObject()
val type = if (isGet) {
"webauthn.get"
} else {
"webauthn.create"
}
clientJson.put("type", type)
clientJson.put("challenge", b64Encode(challenge))
clientJson.put("origin", origin)
if (isCrossOriginAdded) {
clientJson.put("crossOrigin", false)
}
if (packageName != null) {
clientJson.put("androidPackageName", packageName)
}
val clientDataFinal = clientJson.toString().replace("\\/", "/")
return clientDataFinal
}
fun generateClientDataJsonPrivileged(): String {
// will be replaced by the clientData from the privileged app like a browser
return "<placeholder>"
}
fun generateAuthDataForUsage(
rpId: ByteArray,
userPresent: Boolean,
userVerified: Boolean,
backupEligibility: Boolean,
backupState: Boolean,
attestedCredentialData: Boolean = false
): ByteArray {
val rpHash = HashManager.hashSha256(rpId)
// see https://www.w3.org/TR/webauthn-3/#table-authData
var flags = 0
val one = 1
if (userPresent) {
flags = flags or one.shl(0)
}
// bit at index 1 is reserved
if (userVerified) {
flags = flags or one.shl(2)
}
if (backupEligibility) {
flags = flags or one.shl(3)
}
if (backupState) {
flags = flags or one.shl(4)
}
// bit at index 5 is reserved
if (attestedCredentialData) {
flags = flags or one.shl(6)
}
// bit at index 7: Extension data included == false
val signCount = byteArrayOf(0, 0, 0, 0)
return rpHash + byteArrayOf(flags.toByte()) + signCount
}
fun generateAuthDataForCreate(
rpId: ByteArray,
userPresent: Boolean,
userVerified: Boolean,
backupEligibility: Boolean,
backupState: Boolean,
credentialId: ByteArray,
credentialPublicKey: ByteArray
): ByteArray {
val authDataPartOne = generateAuthDataForUsage(
rpId,
userPresent,
userVerified,
backupEligibility,
backupState,
attestedCredentialData = true
)
// Authenticator Attestation Globally Unique Identifier
val aaguid = ByteArray(16) { 0 }
val credIdLen =
byteArrayOf((credentialId.size.shr(8)).toByte(), credentialId.size.toByte())
return authDataPartOne + aaguid + credIdLen + credentialId + credentialPublicKey
}
fun generateDataTosSignNonPrivileged(
clientDataJson: String,
authenticatorData: ByteArray
): ByteArray {
val hash = HashManager.hashSha256(clientDataJson.toByteArray())
return authenticatorData + hash
}
fun generateDataToSignPrivileged(
clientDataHash: ByteArray,
authenticatorData: ByteArray
): ByteArray {
return authenticatorData + clientDataHash
}
fun generateAttestationObject(authData: ByteArray): ByteArray {
val ao = mutableMapOf<String, Any>()
ao["fmt"] = "none"
ao["attStmt"] = emptyMap<Any, Any>()
ao["authData"] = authData
return generateCborFromMap(ao)
}
@SuppressLint("RestrictedApi")
fun <T> generateCborFromMap(map: Map<T, Any>): ByteArray {
return Cbor().encode(map)
}
fun createAuthenticatorAttestationResponseJSON(
credentialId: ByteArray,
clientDataJson: String,
attestationObject: ByteArray,
publicKeyCbor: ByteArray,
authData: ByteArray,
publicKeyTypeId: Long
): String {
// See AuthenticatorAttestationResponseJSON at
// https://www.w3.org/TR/webauthn-3/#ref-for-dom-publickeycredential-tojson
val rk = JSONObject()
// see at https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
val discoverableCredential = true
rk.put("rk", discoverableCredential)
val credProps = JSONObject()
credProps.put("credProps", rk)
val response = JSONObject()
response.put("attestationObject", b64Encode(attestationObject))
response.put("clientDataJSON", clientDataJson)
response.put("transports", JSONArray(listOf("internal", "hybrid")))
response.put("publicKeyAlgorithm", publicKeyTypeId)
response.put("publicKey", b64Encode(publicKeyCbor))
response.put("authenticatorData", b64Encode(authData))
val all = JSONObject()
all.put("id", b64Encode(credentialId))
all.put("rawId", b64Encode(credentialId))
all.put("response", response)
all.put("type", "public-key")
all.put("clientExtensionResults", credProps)
all.put("authenticatorAttachment", "platform")
return all.toString()
}
fun generateGetCredentialResponse(
clientDataJson: ByteArray,
authenticatorData: ByteArray,
signature: ByteArray,
userHandle: String,
id: String
): String {
val response = JSONObject()
response.put("clientDataJSON", b64Encode(clientDataJson))
response.put("authenticatorData", b64Encode(authenticatorData))
response.put("signature", b64Encode(signature))
response.put("userHandle", userHandle)
val ret = JSONObject()
ret.put("id", id)
ret.put("rawId", id)
ret.put("type", "public-key")
ret.put("authenticatorAttachment", "platform")
ret.put("response", response)
ret.put("clientExtensionResults", JSONObject())
return ret.toString()
}
fun parseJsonToRequestOptions(requestJson: String): PublicKeyCredentialRequestOptions {
val jsonObject = JSONObject(requestJson)
val challengeString = jsonObject.getString("challenge")
val relyingParty = jsonObject.optString("rpId", "")
return PublicKeyCredentialRequestOptions(relyingParty, challengeString)
}
fun parseJsonToCreateOptions(requestJson: String): PublicKeyCredentialCreationOptions {
val jsonObject = JSONObject(requestJson)
val rpJson = jsonObject.getJSONObject("rp")
val relyingParty = rpJson.getString("id")
val challenge = b64Decode(jsonObject.getString("challenge"))
val rpUser = jsonObject.getJSONObject("user")
val username = rpUser.getString("name")
val userId = b64Decode(rpUser.getString("id"))
val pubKeyCredParamsJson = jsonObject.getJSONArray("pubKeyCredParams")
val keyTypeIdList: MutableList<Long> = mutableListOf()
for (i in 0 until pubKeyCredParamsJson.length()) {
val e = pubKeyCredParamsJson.getJSONObject(i)
if (e.getString("type") == "public-key") {
keyTypeIdList.add(e.getLong("alg"))
}
}
return PublicKeyCredentialCreationOptions(
relyingParty,
challenge,
username,
userId,
keyTypeIdList.distinct()
)
}
}
}

View File

@@ -1,21 +0,0 @@
package com.kunzisoft.keepass.credentialprovider.passkey.util
import android.content.res.AssetManager
import androidx.credentials.provider.CallingAppInfo
class OriginHelper {
companion object {
const val DEFAULT_PROTOCOL = "https://"
fun getWebOrigin(callingAppInfo: CallingAppInfo?, assets: AssetManager): String? {
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
it.readText()
}
// for trusted browsers like Chrome and Firefox
return callingAppInfo?.getOrigin(privilegedAllowlist)?.removeSuffix("/")
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.keepass.credentialprovider.passkey.util
import android.content.res.AssetManager
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.credentials.provider.CallingAppInfo
@RequiresApi(Build.VERSION_CODES.P)
class OriginManager(
callingAppInfo: CallingAppInfo?,
assets: AssetManager,
private val relyingParty: String
) {
private val webOrigin: String?
private val apkSigningCertificate: ByteArray? = callingAppInfo?.signingInfo?.apkContentsSigners
?.getOrNull(0)?.toByteArray()
init {
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
it.readText()
}
// for trusted browsers like Chrome and Firefox
webOrigin = callingAppInfo?.getOrigin(privilegedAllowlist)?.removeSuffix("/")
}
private fun isPrivilegedApp(): Boolean {
return webOrigin != null
&& webOrigin == (DEFAULT_PROTOCOL + relyingParty)
}
// TODO isPrivileged app
fun checkPrivilegedApp(
clientDataHash: ByteArray?
) {
val isPrivilegedApp = isPrivilegedApp() && clientDataHash != null
Log.d(TAG, "isPrivilegedApp = $isPrivilegedApp")
if (!isPrivilegedApp) {
AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate)
}
}
fun checkPrivilegedApp() {
val isPrivilegedApp = isPrivilegedApp()
Log.d(TAG, "isPrivilegedApp = $isPrivilegedApp")
if (!isPrivilegedApp) {
AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate)
}
}
val origin: String
get() {
return webOrigin ?: (DEFAULT_PROTOCOL + relyingParty)
}
companion object {
private val TAG = OriginManager::class.simpleName
const val DEFAULT_PROTOCOL = "https://"
}
}

View File

@@ -22,6 +22,7 @@ package com.kunzisoft.keepass.credentialprovider.passkey.util
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.AssetManager
import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
@@ -36,11 +37,21 @@ import androidx.credentials.PublicKeyCredential
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.provider.ProviderGetCredentialRequest
import com.kunzisoft.asymmetric.Signature
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataDefinedResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.ClientDataNotDefinedResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.FidoPublicKeyCredential
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
import com.kunzisoft.keepass.credentialprovider.passkey.util.OriginHelper.Companion.DEFAULT_PROTOCOL
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode
import com.kunzisoft.keepass.credentialprovider.passkey.util.OriginManager.Companion.DEFAULT_PROTOCOL
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.EntryInfoPasskey.getPasskey
import com.kunzisoft.keepass.model.Passkey
@@ -206,20 +217,18 @@ object PasskeyHelper {
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
fun Intent.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
val request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(this)
?: throw CreateCredentialUnknownException("could not retrieve request from intent")
fun ProviderCreateCredentialRequest.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
val request = this
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
}
return JsonHelper.parseJsonToCreateOptions(
return PublicKeyCredentialCreationOptions(
(request.callingRequest as CreatePublicKeyCredentialRequest).requestJson
)
}
fun Intent.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(this)
?: throw CreateCredentialUnknownException("could not retrieve request from intent")
fun ProviderGetCredentialRequest.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
val request = this
if (request.credentialOptions.size != 1) {
throw GetCredentialUnknownException("not exact one credentialOption")
}
@@ -230,36 +239,31 @@ object PasskeyHelper {
}
fun retrievePasskeyCreationRequestParameters(
creationOptions: PublicKeyCredentialCreationOptions,
webOrigin: String?,
apkSigningCertificate: ByteArray?,
intent: Intent,
assetManager: AssetManager,
packageName: String?,
passkeyCreated: (Passkey, PublicKeyCredentialCreationParameters) -> Unit
) {
val relyingParty = creationOptions.relyingParty
val username = creationOptions.username
val userHandle = creationOptions.userId
val keyTypeIdList = creationOptions.keyTypeIdList
val challenge = creationOptions.challenge
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
val callingAppInfo = getCredentialRequest?.callingAppInfo
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
if (createCredentialRequest == null)
throw CreateCredentialUnknownException("could not retrieve request from intent")
val creationOptions = createCredentialRequest.retrievePasskeyCreationComponent()
val isPrivilegedApp =
(webOrigin != null && webOrigin == DEFAULT_PROTOCOL + relyingParty)
Log.d(this::class.java.simpleName, "isPrivilegedApp = $isPrivilegedApp")
val relyingParty = creationOptions.relyingPartyEntity.id
val username = creationOptions.userEntity.name
val userHandle = creationOptions.userEntity.id
val pubKeyCredParams = creationOptions.pubKeyCredParams
if (!isPrivilegedApp) {
val isValid =
AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate)
if (!isValid) {
throw CreateCredentialUnknownException(
"could not verify relation between app " +
"and relyingParty $relyingParty"
)
}
}
val originManager = OriginManager(callingAppInfo, assetManager, relyingParty)
originManager.checkPrivilegedApp()
val credentialId = KeePassDXRandom.generateCredentialId()
val (keyPair, keyTypeId) = Signature.generateKeyPair(keyTypeIdList)
?: throw CreateCredentialUnknownException("no known public key type found")
val (keyPair, keyTypeId) = Signature.generateKeyPair(
pubKeyCredParams.map { params -> params.alg }
) ?: throw CreateCredentialUnknownException("no known public key type found")
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
// create new entry in database
@@ -268,68 +272,51 @@ object PasskeyHelper {
username = username,
displayName = "$relyingParty (Passkey)",
privateKeyPem = privateKeyPem,
credentialId = Base64Helper.b64Encode(credentialId),
userHandle = Base64Helper.b64Encode(userHandle),
credentialId = b64Encode(credentialId),
userHandle = b64Encode(userHandle),
relyingParty = DEFAULT_PROTOCOL + relyingParty
),
PublicKeyCredentialCreationParameters(
relyingParty = relyingParty,
challenge = challenge,
publicKeyCredentialCreationOptions = creationOptions,
credentialId = credentialId,
signatureKey = Pair(keyPair, keyTypeId),
isPrivilegedApp = isPrivilegedApp
clientDataResponse = ClientDataNotDefinedResponse(
type = ClientDataNotDefinedResponse.Type.CREATE,
challenge = creationOptions.challenge,
origin = originManager.origin,
packageName = packageName
)
)
)
}
fun buildCreatePublicKeyCredentialResponse(
packageName: String?,
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters
): CreatePublicKeyCredentialResponse {
val keyPair = publicKeyCredentialCreationParameters.signatureKey.first
val keyTypeId = publicKeyCredentialCreationParameters.signatureKey.second
val publicKeyEncoded = Signature.convertPublicKey(keyPair.public, keyTypeId)
val publicKeyMap = Signature.convertPublicKeyToMap(keyPair.public, keyTypeId)
val authData = JsonHelper.generateAuthDataForCreate(
val responseJson = FidoPublicKeyCredential(
id = b64Encode(publicKeyCredentialCreationParameters.credentialId),
response = AuthenticatorAttestationResponse(
requestOptions = publicKeyCredentialCreationParameters.publicKeyCredentialCreationOptions,
credentialId = publicKeyCredentialCreationParameters.credentialId,
credentialPublicKey = Cbor().encode(Signature.convertPublicKeyToMap(
publicKeyIn = keyPair.public,
keyTypeId = keyTypeId
) ?: mapOf<Int, Any>()),
userPresent = true,
userVerified = true,
backupEligibility = true,
backupState = true,
rpId = publicKeyCredentialCreationParameters.relyingParty.toByteArray(),
credentialId = publicKeyCredentialCreationParameters.credentialId,
credentialPublicKey = JsonHelper.generateCborFromMap(publicKeyMap!!)
)
val attestationObject = JsonHelper.generateAttestationObject(authData)
val clientJson: String
if (publicKeyCredentialCreationParameters.isPrivilegedApp) {
clientJson = JsonHelper.generateClientDataJsonPrivileged()
} else {
val origin = DEFAULT_PROTOCOL + publicKeyCredentialCreationParameters.relyingParty
clientJson = JsonHelper.generateClientDataJsonNonPrivileged(
publicKeyCredentialCreationParameters.challenge,
origin,
packageName,
isCrossOriginAdded = true,
isGet = false
)
}
val responseJson = JsonHelper.createAuthenticatorAttestationResponseJSON(
publicKeyCredentialCreationParameters.credentialId,
clientJson,
attestationObject,
publicKeyEncoded!!,
authData,
keyTypeId
)
publicKeyTypeId = keyTypeId,
publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!,
clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse
),
authenticatorAttachment = "platform"
).json()
// log only the length to prevent logging sensitive information
Log.d(javaClass.simpleName, "responseJson with length ${responseJson.length} created")
Log.d(javaClass.simpleName, "Json response for key creation")
return CreatePublicKeyCredentialResponse(responseJson)
}
@@ -338,42 +325,32 @@ object PasskeyHelper {
intent: Intent,
result: (PublicKeyCredentialUsageParameters) -> Unit
) {
val callingAppInfo = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)?.callingAppInfo
val credentialOption = intent.retrievePasskeyUsageComponent()
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
if (getCredentialRequest == null)
throw CreateCredentialUnknownException("could not retrieve request from intent")
val callingAppInfo = getCredentialRequest.callingAppInfo
val credentialOption = getCredentialRequest.retrievePasskeyUsageComponent()
val clientDataHash = credentialOption.clientDataHash
val requestOptions = JsonHelper.parseJsonToRequestOptions(credentialOption.requestJson)
val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
val relyingParty = requestOptions.rpId
val relyingParty = requestOptions.relyingParty
val challenge = Base64Helper.b64Decode(requestOptions.challengeString)
val packageName = callingAppInfo?.packageName
val webOrigin = OriginHelper.getWebOrigin(callingAppInfo, context.assets)
val isPrivilegedApp =
(webOrigin != null && webOrigin == DEFAULT_PROTOCOL + relyingParty && clientDataHash != null)
Log.d(javaClass.simpleName, "isPrivilegedApp = $isPrivilegedApp")
if (!isPrivilegedApp) {
if (!AppRelyingPartyRelation.isRelationValid(
relyingParty,
apkSigningCertificate = callingAppInfo?.signingInfo?.apkContentsSigners
?.getOrNull(0)?.toByteArray()
)) {
throw CreateCredentialUnknownException(
"could not verify relation between app " +
"and relyingParty $relyingParty"
)
}
}
val originManager = OriginManager(callingAppInfo, context.assets, relyingParty)
originManager.checkPrivilegedApp(clientDataHash)
result.invoke(
PublicKeyCredentialUsageParameters(
relyingParty = relyingParty,
packageName = packageName,
clientDataHash = clientDataHash,
isPrivilegedApp = isPrivilegedApp,
challenge = challenge
publicKeyCredentialRequestOptions = requestOptions,
clientDataResponse = clientDataHash?.let {
ClientDataDefinedResponse(clientDataHash)
} ?: run {
ClientDataNotDefinedResponse(
type = ClientDataNotDefinedResponse.Type.GET,
challenge = requestOptions.challenge,
origin = originManager.origin,
packageName = callingAppInfo.packageName
)
}
)
)
}
@@ -382,46 +359,21 @@ object PasskeyHelper {
usageParameters: PublicKeyCredentialUsageParameters,
passkey: Passkey
): PublicKeyCredential {
// https://www.w3.org/TR/webauthn-3/#authdata-flags
val authenticatorData = JsonHelper.generateAuthDataForUsage(
usageParameters.relyingParty.toByteArray(),
val getCredentialResponse = FidoPublicKeyCredential(
id = passkey.credentialId,
response = AuthenticatorAssertionResponse(
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
userPresent = true,
userVerified = true,
backupEligibility = true,
backupState = true
)
val clientDataJson: String
val dataToSign: ByteArray
if (usageParameters.isPrivilegedApp) {
clientDataJson = JsonHelper.generateClientDataJsonPrivileged()
dataToSign =
JsonHelper.generateDataToSignPrivileged(usageParameters.clientDataHash!!, authenticatorData)
} else {
val origin = DEFAULT_PROTOCOL + usageParameters.relyingParty
clientDataJson = JsonHelper.generateClientDataJsonNonPrivileged(
usageParameters.challenge,
origin,
usageParameters.packageName,
isGet = true,
isCrossOriginAdded = false
)
dataToSign =
JsonHelper.generateDataTosSignNonPrivileged(clientDataJson, authenticatorData)
}
val signature = Signature.sign(passkey.privateKeyPem, dataToSign)
?: throw GetCredentialUnknownException("signing failed")
val getCredentialResponse =
JsonHelper.generateGetCredentialResponse(
clientDataJson.toByteArray(),
authenticatorData,
signature,
passkey.userHandle,
passkey.credentialId
)
backupState = true,
userHandle = passkey.userHandle,
privateKey = passkey.privateKeyPem,
clientDataResponse = usageParameters.clientDataResponse
),
authenticatorAttachment = "platform"
).json()
Log.d(javaClass.simpleName, "Json response for key usage")
return PublicKeyCredential(getCredentialResponse)
}

View File

@@ -23,9 +23,7 @@ import java.security.spec.ECGenParameterSpec
object Signature {
// see at https://www.iana.org/assignments/cose/cose.xhtml
const val ES256_ALGORITHM: Long = -7
const val RS256_ALGORITHM: Long = -257
private const val RS256_KEY_SIZE_IN_BITS = 2048
@@ -171,6 +169,4 @@ object Signature {
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")
return null
}
}

View File

@@ -1,3 +1,22 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.random
import java.security.SecureRandom
@@ -15,7 +34,5 @@ class KeePassDXRandom {
internalSecureRandom.nextBytes(credentialId)
return credentialId
}
}
}