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.TypeMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
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.addAuthCode
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildCreatePublicKeyCredentialResponse
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.buildPasskeyPublicKeyCredential
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.removePasskey
|
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.retrieveNodeId
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskey
|
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.retrievePasskeyCreationRequestParameters
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.PasskeyHelper.retrievePasskeyUsageRequestParameters
|
||||||
import com.kunzisoft.keepass.database.ContextualDatabase
|
import com.kunzisoft.keepass.database.ContextualDatabase
|
||||||
@@ -115,7 +113,6 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
|
|||||||
PendingIntentHandler.setCreateCredentialResponse(
|
PendingIntentHandler.setCreateCredentialResponse(
|
||||||
intent = responseIntent,
|
intent = responseIntent,
|
||||||
response = buildCreatePublicKeyCredentialResponse(
|
response = buildCreatePublicKeyCredentialResponse(
|
||||||
packageName = packageName,
|
|
||||||
publicKeyCredentialCreationParameters = it
|
publicKeyCredentialCreationParameters = it
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -183,7 +180,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
|
|||||||
finish()
|
finish()
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
|
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,7 +241,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
|
|||||||
finish()
|
finish()
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
|
Log.e(TAG, "Unable to auto select passkey, usage parameters are empty")
|
||||||
setResult(Activity.RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,67 +251,62 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
|
|||||||
searchInfo: SearchInfo
|
searchInfo: SearchInfo
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "Launch passkey registration")
|
Log.d(TAG, "Launch passkey registration")
|
||||||
PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)?.callingAppInfo?.let { callingAppInfo ->
|
retrievePasskeyCreationRequestParameters(
|
||||||
retrievePasskeyCreationRequestParameters(
|
intent = intent,
|
||||||
creationOptions = intent.retrievePasskeyCreationComponent(),
|
assetManager = assets,
|
||||||
webOrigin = OriginHelper.getWebOrigin(callingAppInfo, assets),
|
packageName = packageName,
|
||||||
apkSigningCertificate =
|
passkeyCreated = { passkey, publicKeyCredentialParameters ->
|
||||||
callingAppInfo
|
// Save the requested parameters
|
||||||
.signingInfo.apkContentsSigners
|
mPasskey = passkey
|
||||||
.getOrNull(0)?.toByteArray(),
|
mCreationParameters = publicKeyCredentialParameters
|
||||||
passkeyCreated = { passkey, publicKeyCredentialParameters ->
|
// Manage the passkey and create a register info
|
||||||
// Save the requested parameters
|
val registerInfo = RegisterInfo(
|
||||||
mPasskey = passkey
|
searchInfo = searchInfo,
|
||||||
mCreationParameters = publicKeyCredentialParameters
|
username = null,
|
||||||
// Manage the passkey and create a register info
|
passkey = passkey
|
||||||
val registerInfo = RegisterInfo(
|
)
|
||||||
|
// If nodeId already provided
|
||||||
|
intent.retrieveNodeId()?.let { nodeId ->
|
||||||
|
autoRegisterPasskeyAndSetResult(database, nodeId)
|
||||||
|
} ?: run {
|
||||||
|
SearchHelper.checkAutoSearchInfo(
|
||||||
|
context = this,
|
||||||
|
database = database,
|
||||||
searchInfo = searchInfo,
|
searchInfo = searchInfo,
|
||||||
username = null,
|
onItemsFound = { openedDatabase, _ ->
|
||||||
passkey = passkey
|
Log.w(TAG, "Passkey found for registration, " +
|
||||||
|
"but launch manual registration for a new entry")
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onItemNotFound = { openedDatabase ->
|
||||||
|
Log.d(TAG, "Launch new manual registration in opened database")
|
||||||
|
GroupActivity.launchForRegistration(
|
||||||
|
context = this,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||||
|
database = openedDatabase,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDatabaseClosed = {
|
||||||
|
Log.d(TAG, "Manual passkey registration in closed database")
|
||||||
|
FileDatabaseSelectActivity.launchForRegistration(
|
||||||
|
context = this,
|
||||||
|
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
||||||
|
registerInfo = registerInfo,
|
||||||
|
typeMode = TypeMode.PASSKEY
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
// If nodeId already provided
|
|
||||||
intent.retrieveNodeId()?.let { nodeId ->
|
|
||||||
autoRegisterPasskeyAndSetResult(database, nodeId)
|
|
||||||
} ?: run {
|
|
||||||
SearchHelper.checkAutoSearchInfo(
|
|
||||||
context = this,
|
|
||||||
database = database,
|
|
||||||
searchInfo = searchInfo,
|
|
||||||
onItemsFound = { openedDatabase, _ ->
|
|
||||||
Log.w(TAG, "Passkey found for registration, " +
|
|
||||||
"but launch manual registration for a new entry")
|
|
||||||
GroupActivity.launchForRegistration(
|
|
||||||
context = this,
|
|
||||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
|
||||||
database = openedDatabase,
|
|
||||||
registerInfo = registerInfo,
|
|
||||||
typeMode = TypeMode.PASSKEY
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onItemNotFound = { openedDatabase ->
|
|
||||||
Log.d(TAG, "Launch new manual registration in opened database")
|
|
||||||
GroupActivity.launchForRegistration(
|
|
||||||
context = this,
|
|
||||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
|
||||||
database = openedDatabase,
|
|
||||||
registerInfo = registerInfo,
|
|
||||||
typeMode = TypeMode.PASSKEY
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onDatabaseClosed = {
|
|
||||||
Log.d(TAG, "Manual passkey registration in closed database")
|
|
||||||
FileDatabaseSelectActivity.launchForRegistration(
|
|
||||||
context = this,
|
|
||||||
activityResultLauncher = mPasskeyRegistrationActivityResultLauncher,
|
|
||||||
registerInfo = registerInfo,
|
|
||||||
typeMode = TypeMode.PASSKEY
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ import com.kunzisoft.keepass.R
|
|||||||
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
import com.kunzisoft.keepass.credentialprovider.EntrySelectionHelper.buildIcon
|
||||||
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
import com.kunzisoft.keepass.credentialprovider.SpecialMode
|
||||||
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
import com.kunzisoft.keepass.credentialprovider.activity.PasskeyLauncherActivity
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.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.ContextualDatabase
|
||||||
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
import com.kunzisoft.keepass.database.DatabaseTaskProvider
|
||||||
import com.kunzisoft.keepass.database.helper.SearchHelper
|
import com.kunzisoft.keepass.database.helper.SearchHelper
|
||||||
@@ -117,13 +118,11 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
|
|
||||||
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
|
||||||
|
|
||||||
val relyingPartyJson = JsonHelper
|
val relyingPartyId = PublicKeyCredentialRequestOptions(option.requestJson).rpId
|
||||||
.parseJsonToRequestOptions(option.requestJson)
|
|
||||||
.relyingParty
|
|
||||||
val searchInfo = SearchInfo().apply {
|
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(
|
SearchHelper.checkAutoSearchInfo(
|
||||||
context = this,
|
context = this,
|
||||||
database = mDatabase,
|
database = mDatabase,
|
||||||
@@ -154,7 +153,7 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onItemNotFound = { _ ->
|
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")
|
Log.d(TAG, "Add pending intent for passkey selection in opened database")
|
||||||
PasskeyLauncherActivity.getPendingIntent(
|
PasskeyLauncherActivity.getPendingIntent(
|
||||||
context = applicationContext,
|
context = applicationContext,
|
||||||
@@ -252,11 +251,11 @@ class PasskeyProviderService : CredentialProviderService() {
|
|||||||
|
|
||||||
val accountName = mDatabase?.name ?: getString(R.string.passkey_locked_database_username)
|
val accountName = mDatabase?.name ?: getString(R.string.passkey_locked_database_username)
|
||||||
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
val createEntries: MutableList<CreateEntry> = mutableListOf()
|
||||||
|
val relyingPartyId = PublicKeyCredentialCreationOptions(request.requestJson).relyingPartyEntity.id
|
||||||
val searchInfo = SearchInfo().apply {
|
val searchInfo = SearchInfo().apply {
|
||||||
relyingParty = JsonHelper
|
relyingParty = relyingPartyId
|
||||||
.parseJsonToCreateOptions(request.requestJson)
|
|
||||||
.relyingParty
|
|
||||||
}
|
}
|
||||||
|
Log.d(TAG, "Build passkey search for relying party $relyingPartyId")
|
||||||
SearchHelper.checkAutoSearchInfo(
|
SearchHelper.checkAutoSearchInfo(
|
||||||
context = this,
|
context = this,
|
||||||
database = mDatabase,
|
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
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
data class PublicKeyCredentialCreationOptions(
|
import android.util.Log
|
||||||
val relyingParty: String,
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper
|
||||||
val challenge: ByteArray, // TODO Equals Hashcode
|
import org.json.JSONObject
|
||||||
val username: String,
|
|
||||||
val userId: ByteArray, // TODO Equals Hashcode
|
class PublicKeyCredentialCreationOptions(requestJson: String) {
|
||||||
val keyTypeIdList: List<Long>
|
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
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
|
|
||||||
data class PublicKeyCredentialCreationParameters(
|
data class PublicKeyCredentialCreationParameters(
|
||||||
val relyingParty: String,
|
val publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions,
|
||||||
val credentialId: ByteArray, // TODO Equals Hashcode
|
val credentialId: ByteArray, // TODO Equals Hashcode
|
||||||
val signatureKey: Pair<KeyPair, Long>,
|
val signatureKey: Pair<KeyPair, Long>,
|
||||||
val isPrivilegedApp: Boolean,
|
val clientDataResponse: ClientDataResponse
|
||||||
val challenge: ByteArray, // TODO Equals Hashcode
|
|
||||||
)
|
)
|
||||||
@@ -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
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
data class PublicKeyCredentialRequestOptions(
|
import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper
|
||||||
val relyingParty: String,
|
import org.json.JSONObject
|
||||||
val challengeString: String
|
|
||||||
)
|
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
|
package com.kunzisoft.keepass.credentialprovider.passkey.data
|
||||||
|
|
||||||
data class PublicKeyCredentialUsageParameters(
|
data class PublicKeyCredentialUsageParameters(
|
||||||
val relyingParty: String,
|
val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions,
|
||||||
val packageName: String? = null,
|
val clientDataResponse: ClientDataResponse
|
||||||
val clientDataHash: ByteArray?, // TODO Equals Hashcode
|
|
||||||
val isPrivilegedApp: Boolean,
|
|
||||||
val challenge: ByteArray, // TODO Equals Hashcode
|
|
||||||
)
|
)
|
||||||
@@ -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
|
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Base64
|
import android.util.Base64
|
||||||
|
|
||||||
class Base64Helper {
|
class Base64Helper {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun b64Decode(encodedString: String?): ByteArray {
|
fun b64Decode(encodedString: String): ByteArray {
|
||||||
return Base64.decodeBase64(encodedString)
|
return Base64.decode(
|
||||||
|
encodedString,
|
||||||
|
Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun b64Encode(data: ByteArray): String {
|
fun b64Encode(data: ByteArray): String {
|
||||||
return android.util.Base64.encodeToString(
|
return Base64.encodeToString(
|
||||||
data,
|
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.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.res.AssetManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.ParcelUuid
|
import android.os.ParcelUuid
|
||||||
@@ -36,11 +37,21 @@ import androidx.credentials.PublicKeyCredential
|
|||||||
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
import androidx.credentials.exceptions.CreateCredentialUnknownException
|
||||||
import androidx.credentials.exceptions.GetCredentialUnknownException
|
import androidx.credentials.exceptions.GetCredentialUnknownException
|
||||||
import androidx.credentials.provider.PendingIntentHandler
|
import androidx.credentials.provider.PendingIntentHandler
|
||||||
|
import androidx.credentials.provider.ProviderCreateCredentialRequest
|
||||||
|
import androidx.credentials.provider.ProviderGetCredentialRequest
|
||||||
import com.kunzisoft.asymmetric.Signature
|
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.PublicKeyCredentialCreationOptions
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialCreationParameters
|
||||||
|
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions
|
||||||
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialUsageParameters
|
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.EntryInfo
|
||||||
import com.kunzisoft.keepass.model.EntryInfoPasskey.getPasskey
|
import com.kunzisoft.keepass.model.EntryInfoPasskey.getPasskey
|
||||||
import com.kunzisoft.keepass.model.Passkey
|
import com.kunzisoft.keepass.model.Passkey
|
||||||
@@ -206,20 +217,18 @@ object PasskeyHelper {
|
|||||||
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Intent.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
|
fun ProviderCreateCredentialRequest.retrievePasskeyCreationComponent(): PublicKeyCredentialCreationOptions {
|
||||||
val request = PendingIntentHandler.retrieveProviderCreateCredentialRequest(this)
|
val request = this
|
||||||
?: throw CreateCredentialUnknownException("could not retrieve request from intent")
|
|
||||||
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
|
if (request.callingRequest !is CreatePublicKeyCredentialRequest) {
|
||||||
throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
|
throw CreateCredentialUnknownException("callingRequest is of wrong type: ${request.callingRequest.type}")
|
||||||
}
|
}
|
||||||
return JsonHelper.parseJsonToCreateOptions(
|
return PublicKeyCredentialCreationOptions(
|
||||||
(request.callingRequest as CreatePublicKeyCredentialRequest).requestJson
|
(request.callingRequest as CreatePublicKeyCredentialRequest).requestJson
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Intent.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
|
fun ProviderGetCredentialRequest.retrievePasskeyUsageComponent(): GetPublicKeyCredentialOption {
|
||||||
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(this)
|
val request = this
|
||||||
?: throw CreateCredentialUnknownException("could not retrieve request from intent")
|
|
||||||
if (request.credentialOptions.size != 1) {
|
if (request.credentialOptions.size != 1) {
|
||||||
throw GetCredentialUnknownException("not exact one credentialOption")
|
throw GetCredentialUnknownException("not exact one credentialOption")
|
||||||
}
|
}
|
||||||
@@ -230,36 +239,31 @@ object PasskeyHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun retrievePasskeyCreationRequestParameters(
|
fun retrievePasskeyCreationRequestParameters(
|
||||||
creationOptions: PublicKeyCredentialCreationOptions,
|
intent: Intent,
|
||||||
webOrigin: String?,
|
assetManager: AssetManager,
|
||||||
apkSigningCertificate: ByteArray?,
|
packageName: String?,
|
||||||
passkeyCreated: (Passkey, PublicKeyCredentialCreationParameters) -> Unit
|
passkeyCreated: (Passkey, PublicKeyCredentialCreationParameters) -> Unit
|
||||||
) {
|
) {
|
||||||
val relyingParty = creationOptions.relyingParty
|
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||||
val username = creationOptions.username
|
val callingAppInfo = getCredentialRequest?.callingAppInfo
|
||||||
val userHandle = creationOptions.userId
|
val createCredentialRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
|
||||||
val keyTypeIdList = creationOptions.keyTypeIdList
|
if (createCredentialRequest == null)
|
||||||
val challenge = creationOptions.challenge
|
throw CreateCredentialUnknownException("could not retrieve request from intent")
|
||||||
|
val creationOptions = createCredentialRequest.retrievePasskeyCreationComponent()
|
||||||
|
|
||||||
val isPrivilegedApp =
|
val relyingParty = creationOptions.relyingPartyEntity.id
|
||||||
(webOrigin != null && webOrigin == DEFAULT_PROTOCOL + relyingParty)
|
val username = creationOptions.userEntity.name
|
||||||
Log.d(this::class.java.simpleName, "isPrivilegedApp = $isPrivilegedApp")
|
val userHandle = creationOptions.userEntity.id
|
||||||
|
val pubKeyCredParams = creationOptions.pubKeyCredParams
|
||||||
|
|
||||||
if (!isPrivilegedApp) {
|
val originManager = OriginManager(callingAppInfo, assetManager, relyingParty)
|
||||||
val isValid =
|
originManager.checkPrivilegedApp()
|
||||||
AppRelyingPartyRelation.isRelationValid(relyingParty, apkSigningCertificate)
|
|
||||||
if (!isValid) {
|
|
||||||
throw CreateCredentialUnknownException(
|
|
||||||
"could not verify relation between app " +
|
|
||||||
"and relyingParty $relyingParty"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val credentialId = KeePassDXRandom.generateCredentialId()
|
val credentialId = KeePassDXRandom.generateCredentialId()
|
||||||
|
|
||||||
val (keyPair, keyTypeId) = Signature.generateKeyPair(keyTypeIdList)
|
val (keyPair, keyTypeId) = Signature.generateKeyPair(
|
||||||
?: throw CreateCredentialUnknownException("no known public key type found")
|
pubKeyCredParams.map { params -> params.alg }
|
||||||
|
) ?: throw CreateCredentialUnknownException("no known public key type found")
|
||||||
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
|
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
|
||||||
|
|
||||||
// create new entry in database
|
// create new entry in database
|
||||||
@@ -268,68 +272,51 @@ object PasskeyHelper {
|
|||||||
username = username,
|
username = username,
|
||||||
displayName = "$relyingParty (Passkey)",
|
displayName = "$relyingParty (Passkey)",
|
||||||
privateKeyPem = privateKeyPem,
|
privateKeyPem = privateKeyPem,
|
||||||
credentialId = Base64Helper.b64Encode(credentialId),
|
credentialId = b64Encode(credentialId),
|
||||||
userHandle = Base64Helper.b64Encode(userHandle),
|
userHandle = b64Encode(userHandle),
|
||||||
relyingParty = DEFAULT_PROTOCOL + relyingParty
|
relyingParty = DEFAULT_PROTOCOL + relyingParty
|
||||||
),
|
),
|
||||||
PublicKeyCredentialCreationParameters(
|
PublicKeyCredentialCreationParameters(
|
||||||
relyingParty = relyingParty,
|
publicKeyCredentialCreationOptions = creationOptions,
|
||||||
challenge = challenge,
|
|
||||||
credentialId = credentialId,
|
credentialId = credentialId,
|
||||||
signatureKey = Pair(keyPair, keyTypeId),
|
signatureKey = Pair(keyPair, keyTypeId),
|
||||||
isPrivilegedApp = isPrivilegedApp
|
clientDataResponse = ClientDataNotDefinedResponse(
|
||||||
|
type = ClientDataNotDefinedResponse.Type.CREATE,
|
||||||
|
challenge = creationOptions.challenge,
|
||||||
|
origin = originManager.origin,
|
||||||
|
packageName = packageName
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun buildCreatePublicKeyCredentialResponse(
|
fun buildCreatePublicKeyCredentialResponse(
|
||||||
packageName: String?,
|
|
||||||
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters
|
publicKeyCredentialCreationParameters: PublicKeyCredentialCreationParameters
|
||||||
): CreatePublicKeyCredentialResponse {
|
): CreatePublicKeyCredentialResponse {
|
||||||
|
|
||||||
val keyPair = publicKeyCredentialCreationParameters.signatureKey.first
|
val keyPair = publicKeyCredentialCreationParameters.signatureKey.first
|
||||||
val keyTypeId = publicKeyCredentialCreationParameters.signatureKey.second
|
val keyTypeId = publicKeyCredentialCreationParameters.signatureKey.second
|
||||||
|
val responseJson = FidoPublicKeyCredential(
|
||||||
val publicKeyEncoded = Signature.convertPublicKey(keyPair.public, keyTypeId)
|
id = b64Encode(publicKeyCredentialCreationParameters.credentialId),
|
||||||
val publicKeyMap = Signature.convertPublicKeyToMap(keyPair.public, keyTypeId)
|
response = AuthenticatorAttestationResponse(
|
||||||
|
requestOptions = publicKeyCredentialCreationParameters.publicKeyCredentialCreationOptions,
|
||||||
val authData = JsonHelper.generateAuthDataForCreate(
|
credentialId = publicKeyCredentialCreationParameters.credentialId,
|
||||||
userPresent = true,
|
credentialPublicKey = Cbor().encode(Signature.convertPublicKeyToMap(
|
||||||
userVerified = true,
|
publicKeyIn = keyPair.public,
|
||||||
backupEligibility = true,
|
keyTypeId = keyTypeId
|
||||||
backupState = true,
|
) ?: mapOf<Int, Any>()),
|
||||||
rpId = publicKeyCredentialCreationParameters.relyingParty.toByteArray(),
|
userPresent = true,
|
||||||
credentialId = publicKeyCredentialCreationParameters.credentialId,
|
userVerified = true,
|
||||||
credentialPublicKey = JsonHelper.generateCborFromMap(publicKeyMap!!)
|
backupEligibility = true,
|
||||||
)
|
backupState = true,
|
||||||
|
publicKeyTypeId = keyTypeId,
|
||||||
val attestationObject = JsonHelper.generateAttestationObject(authData)
|
publicKeyCbor = Signature.convertPublicKey(keyPair.public, keyTypeId)!!,
|
||||||
|
clientDataResponse = publicKeyCredentialCreationParameters.clientDataResponse
|
||||||
val clientJson: String
|
),
|
||||||
if (publicKeyCredentialCreationParameters.isPrivilegedApp) {
|
authenticatorAttachment = "platform"
|
||||||
clientJson = JsonHelper.generateClientDataJsonPrivileged()
|
).json()
|
||||||
} else {
|
|
||||||
val origin = DEFAULT_PROTOCOL + publicKeyCredentialCreationParameters.relyingParty
|
|
||||||
clientJson = JsonHelper.generateClientDataJsonNonPrivileged(
|
|
||||||
publicKeyCredentialCreationParameters.challenge,
|
|
||||||
origin,
|
|
||||||
packageName,
|
|
||||||
isCrossOriginAdded = true,
|
|
||||||
isGet = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val responseJson = JsonHelper.createAuthenticatorAttestationResponseJSON(
|
|
||||||
publicKeyCredentialCreationParameters.credentialId,
|
|
||||||
clientJson,
|
|
||||||
attestationObject,
|
|
||||||
publicKeyEncoded!!,
|
|
||||||
authData,
|
|
||||||
keyTypeId
|
|
||||||
)
|
|
||||||
|
|
||||||
// log only the length to prevent logging sensitive information
|
// log 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)
|
return CreatePublicKeyCredentialResponse(responseJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,42 +325,32 @@ object PasskeyHelper {
|
|||||||
intent: Intent,
|
intent: Intent,
|
||||||
result: (PublicKeyCredentialUsageParameters) -> Unit
|
result: (PublicKeyCredentialUsageParameters) -> Unit
|
||||||
) {
|
) {
|
||||||
val callingAppInfo = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)?.callingAppInfo
|
val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
|
||||||
val credentialOption = intent.retrievePasskeyUsageComponent()
|
if (getCredentialRequest == null)
|
||||||
|
throw CreateCredentialUnknownException("could not retrieve request from intent")
|
||||||
|
val callingAppInfo = getCredentialRequest.callingAppInfo
|
||||||
|
val credentialOption = getCredentialRequest.retrievePasskeyUsageComponent()
|
||||||
val clientDataHash = credentialOption.clientDataHash
|
val clientDataHash = credentialOption.clientDataHash
|
||||||
|
|
||||||
val requestOptions = JsonHelper.parseJsonToRequestOptions(credentialOption.requestJson)
|
val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
|
||||||
|
val relyingParty = requestOptions.rpId
|
||||||
|
|
||||||
val relyingParty = requestOptions.relyingParty
|
val originManager = OriginManager(callingAppInfo, context.assets, relyingParty)
|
||||||
val challenge = Base64Helper.b64Decode(requestOptions.challengeString)
|
originManager.checkPrivilegedApp(clientDataHash)
|
||||||
val packageName = callingAppInfo?.packageName
|
|
||||||
val webOrigin = OriginHelper.getWebOrigin(callingAppInfo, context.assets)
|
|
||||||
|
|
||||||
val isPrivilegedApp =
|
|
||||||
(webOrigin != null && webOrigin == DEFAULT_PROTOCOL + relyingParty && clientDataHash != null)
|
|
||||||
|
|
||||||
Log.d(javaClass.simpleName, "isPrivilegedApp = $isPrivilegedApp")
|
|
||||||
|
|
||||||
if (!isPrivilegedApp) {
|
|
||||||
if (!AppRelyingPartyRelation.isRelationValid(
|
|
||||||
relyingParty,
|
|
||||||
apkSigningCertificate = callingAppInfo?.signingInfo?.apkContentsSigners
|
|
||||||
?.getOrNull(0)?.toByteArray()
|
|
||||||
)) {
|
|
||||||
throw CreateCredentialUnknownException(
|
|
||||||
"could not verify relation between app " +
|
|
||||||
"and relyingParty $relyingParty"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.invoke(
|
result.invoke(
|
||||||
PublicKeyCredentialUsageParameters(
|
PublicKeyCredentialUsageParameters(
|
||||||
relyingParty = relyingParty,
|
publicKeyCredentialRequestOptions = requestOptions,
|
||||||
packageName = packageName,
|
clientDataResponse = clientDataHash?.let {
|
||||||
clientDataHash = clientDataHash,
|
ClientDataDefinedResponse(clientDataHash)
|
||||||
isPrivilegedApp = isPrivilegedApp,
|
} ?: run {
|
||||||
challenge = challenge
|
ClientDataNotDefinedResponse(
|
||||||
|
type = ClientDataNotDefinedResponse.Type.GET,
|
||||||
|
challenge = requestOptions.challenge,
|
||||||
|
origin = originManager.origin,
|
||||||
|
packageName = callingAppInfo.packageName
|
||||||
|
)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -382,46 +359,21 @@ object PasskeyHelper {
|
|||||||
usageParameters: PublicKeyCredentialUsageParameters,
|
usageParameters: PublicKeyCredentialUsageParameters,
|
||||||
passkey: Passkey
|
passkey: Passkey
|
||||||
): PublicKeyCredential {
|
): PublicKeyCredential {
|
||||||
|
val getCredentialResponse = FidoPublicKeyCredential(
|
||||||
// https://www.w3.org/TR/webauthn-3/#authdata-flags
|
id = passkey.credentialId,
|
||||||
val authenticatorData = JsonHelper.generateAuthDataForUsage(
|
response = AuthenticatorAssertionResponse(
|
||||||
usageParameters.relyingParty.toByteArray(),
|
requestOptions = usageParameters.publicKeyCredentialRequestOptions,
|
||||||
userPresent = true,
|
userPresent = true,
|
||||||
userVerified = true,
|
userVerified = true,
|
||||||
backupEligibility = true,
|
backupEligibility = true,
|
||||||
backupState = true
|
backupState = true,
|
||||||
)
|
userHandle = passkey.userHandle,
|
||||||
|
privateKey = passkey.privateKeyPem,
|
||||||
val clientDataJson: String
|
clientDataResponse = usageParameters.clientDataResponse
|
||||||
val dataToSign: ByteArray
|
),
|
||||||
if (usageParameters.isPrivilegedApp) {
|
authenticatorAttachment = "platform"
|
||||||
clientDataJson = JsonHelper.generateClientDataJsonPrivileged()
|
).json()
|
||||||
dataToSign =
|
Log.d(javaClass.simpleName, "Json response for key usage")
|
||||||
JsonHelper.generateDataToSignPrivileged(usageParameters.clientDataHash!!, authenticatorData)
|
|
||||||
} else {
|
|
||||||
val origin = DEFAULT_PROTOCOL + usageParameters.relyingParty
|
|
||||||
clientDataJson = JsonHelper.generateClientDataJsonNonPrivileged(
|
|
||||||
usageParameters.challenge,
|
|
||||||
origin,
|
|
||||||
usageParameters.packageName,
|
|
||||||
isGet = true,
|
|
||||||
isCrossOriginAdded = false
|
|
||||||
)
|
|
||||||
dataToSign =
|
|
||||||
JsonHelper.generateDataTosSignNonPrivileged(clientDataJson, authenticatorData)
|
|
||||||
}
|
|
||||||
|
|
||||||
val signature = Signature.sign(passkey.privateKeyPem, dataToSign)
|
|
||||||
?: throw GetCredentialUnknownException("signing failed")
|
|
||||||
|
|
||||||
val getCredentialResponse =
|
|
||||||
JsonHelper.generateGetCredentialResponse(
|
|
||||||
clientDataJson.toByteArray(),
|
|
||||||
authenticatorData,
|
|
||||||
signature,
|
|
||||||
passkey.userHandle,
|
|
||||||
passkey.credentialId
|
|
||||||
)
|
|
||||||
return PublicKeyCredential(getCredentialResponse)
|
return PublicKeyCredential(getCredentialResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,7 @@ import java.security.spec.ECGenParameterSpec
|
|||||||
object Signature {
|
object Signature {
|
||||||
|
|
||||||
// see at https://www.iana.org/assignments/cose/cose.xhtml
|
// see at https://www.iana.org/assignments/cose/cose.xhtml
|
||||||
|
|
||||||
const val ES256_ALGORITHM: Long = -7
|
const val ES256_ALGORITHM: Long = -7
|
||||||
|
|
||||||
const val RS256_ALGORITHM: Long = -257
|
const val RS256_ALGORITHM: Long = -257
|
||||||
private const val RS256_KEY_SIZE_IN_BITS = 2048
|
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")
|
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")
|
||||||
return null
|
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
|
package com.kunzisoft.random
|
||||||
|
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
@@ -15,7 +34,5 @@ class KeePassDXRandom {
|
|||||||
internalSecureRandom.nextBytes(credentialId)
|
internalSecureRandom.nextBytes(credentialId)
|
||||||
return credentialId
|
return credentialId
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user