mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Refactiring JSON objects
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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>"
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("/")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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://"
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user