fix: Refactoring verification

This commit is contained in:
J-Jamet
2025-09-01 15:29:19 +02:00
parent 200881278c
commit 7e41527cfe
5 changed files with 104 additions and 202 deletions

View File

@@ -170,7 +170,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() {
finish()
}) {
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
val nodeId = intent.retrieveNodeId()
checkSecurity(intent, nodeId)
when (mSpecialMode) {

View File

@@ -1,168 +0,0 @@
/*
* 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
import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints
import com.kunzisoft.keepass.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.WebOrigin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Utility class to manage the origin of credential provider applications
*/
@RequiresApi(Build.VERSION_CODES.P)
class OriginManager(
private val providedClientDataHash: ByteArray?,
private val callingAppInfo: CallingAppInfo?,
private val assets: AssetManager,
private val relyingParty: String,
) {
/**
* Retrieves the Android origin to be stored in the database,
* call [onOriginRetrieved] if the origin is already calculated by the system
* call [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
*/
suspend fun getOriginAtCreation(
onOriginRetrieved: (appInfoToStore: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginCreated: (appInfoToStore: AppOrigin, origin: String) -> Unit
) {
getOrigin(
onOriginRetrieved = { androidOrigin, webOrigin, callOrigin, clientDataHash ->
onOriginRetrieved(
AppOrigin().apply {
addAndroidOrigin(androidOrigin)
addWebOrigin(webOrigin)
},
clientDataHash
)
},
onOriginNotRetrieved = { appIdentifier, webOrigin ->
// Create a new Android Origin and prepare the signature app storage
onOriginCreated(
AppOrigin().apply {
addAndroidOrigin(appIdentifier)
addWebOrigin(webOrigin)
},
appIdentifier.toAndroidOrigin()
)
}
)
}
/**
* Retrieves the origin to verify usage,
* calls [onOriginRetrieved] if the origin is already calculated by the system
* calls [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database
*/
suspend fun getOriginAtUsage(
onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginCreated: (appOrigin: AppOrigin) -> Unit
) {
getOrigin(
onOriginRetrieved = { androidOrigin, webOrigin, origin, clientDataHash ->
onOriginRetrieved(
AppOrigin().apply {
addAndroidOrigin(androidOrigin)
addWebOrigin(webOrigin)
},
clientDataHash
)
},
onOriginNotRetrieved = { androidOrigin, webOrigin ->
// Check the app signature in the appOrigin, webOrigin cannot be checked now
onOriginCreated(
AppOrigin().apply {
addAndroidOrigin(androidOrigin)
addWebOrigin(webOrigin)
}
)
}
)
}
/**
* Utility method to retrieve the origin asynchronously,
* checks for the presence of the application in the privilege list of the trustedPackages.json file,
* call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list
* call [onOriginNotRetrieved] if the origin is not retrieved from the system
*/
private suspend fun getOrigin(
onOriginRetrieved: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin, origin: String, clientDataHash: ByteArray) -> Unit,
onOriginNotRetrieved: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin) -> Unit
) {
if (callingAppInfo == null) {
throw SecurityException("Calling app info cannot be retrieved")
}
withContext(Dispatchers.IO) {
var callOrigin: String?
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
it.readText()
}
// for trusted browsers like Chrome and Firefox
callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
val androidOrigin = AndroidOrigin(
packageName = callingAppInfo.packageName,
fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints(),
verified = false
)
val webOrigin = WebOrigin.fromRelyingParty(
relyingParty = relyingParty,
verified = false
)
// Check if the webDomain is validated for the
withContext(Dispatchers.Main) {
if (callOrigin != null && providedClientDataHash != null) {
Log.d(TAG, "Origin $callOrigin retrieved from callingAppInfo")
// TODO verified callOrigin
onOriginRetrieved(
AndroidOrigin(
packageName = androidOrigin.packageName,
fingerprint = androidOrigin.fingerprint,
verified = true
),
WebOrigin(
origin = webOrigin.origin,
verified = true
),
callOrigin,
providedClientDataHash
)
} else {
onOriginNotRetrieved(
androidOrigin,
webOrigin
)
}
}
}
}
companion object {
private val TAG = OriginManager::class.simpleName
}
}

View File

@@ -35,11 +35,13 @@ import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.PublicKeyCredential
import androidx.credentials.exceptions.CreateCredentialUnknownException
import androidx.credentials.exceptions.GetCredentialUnknownException
import androidx.credentials.provider.CallingAppInfo
import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.provider.ProviderGetCredentialRequest
import com.kunzisoft.asymmetric.Signature
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
@@ -51,13 +53,17 @@ import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredential
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.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.WebOrigin
import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.random.KeePassDXRandom
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.security.KeyStore
import java.security.MessageDigest
import java.time.Instant
@@ -320,6 +326,59 @@ object PasskeyHelper {
return request.credentialOptions[0] as GetPublicKeyCredentialOption
}
/**
* Utility method to retrieve the origin asynchronously,
* checks for the presence of the application in the privilege list of the trustedPackages.json file,
* call [onOriginRetrieved] if the origin is already calculated by the system and in the privileged list, return the clientDataHash
* call [onOriginNotRetrieved] if the origin is not retrieved from the system, return a new Android Origin
*/
suspend fun getOrigin(
providedClientDataHash: ByteArray?,
callingAppInfo: CallingAppInfo?,
assets: AssetManager,
relyingParty: String,
onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginNotRetrieved: (appOrigin: AppOrigin, androidOriginString: String) -> Unit
) {
if (callingAppInfo == null) {
throw SecurityException("Calling app info cannot be retrieved")
}
withContext(Dispatchers.IO) {
var callOrigin: String?
val privilegedAllowlist = assets.open("trustedPackages.json").bufferedReader().use {
it.readText()
}
// for trusted browsers like Chrome and Firefox
callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/")
val androidOrigin = AndroidOrigin(
packageName = callingAppInfo.packageName,
fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
)
val webOrigin = WebOrigin.fromRelyingParty(
relyingParty = relyingParty
)
// Check if the webDomain is validated for the
withContext(Dispatchers.Main) {
if (callOrigin != null && providedClientDataHash != null) {
// Origin already defined by the system
Log.d(javaClass.simpleName, "Origin $callOrigin retrieved from callingAppInfo")
onOriginRetrieved(
AppOrigin.fromOrigin(callOrigin, androidOrigin, verified = true),
providedClientDataHash
)
} else {
// Add Android origin by default
onOriginNotRetrieved(
AppOrigin(verified = false).apply {
addAndroidOrigin(androidOrigin)
},
androidOrigin.toAndroidOrigin()
)
}
}
}
}
/**
* Utility method to create a passkey and the associated creation request parameters
* [intent] allows to retrieve the request
@@ -360,12 +419,11 @@ object PasskeyHelper {
)
// create new entry in database
OriginManager(
getOrigin(
providedClientDataHash = clientDataHash,
callingAppInfo = callingAppInfo,
assets = assetManager,
relyingParty = relyingParty
).getOriginAtCreation(
relyingParty = relyingParty,
onOriginRetrieved = { appInfoToStore, clientDataHash ->
passkeyCreated.invoke(
passkey,
@@ -378,7 +436,7 @@ object PasskeyHelper {
)
)
},
onOriginCreated = { appInfoToStore, origin ->
onOriginNotRetrieved = { appInfoToStore, origin ->
passkeyCreated.invoke(
passkey,
appInfoToStore,
@@ -451,12 +509,11 @@ object PasskeyHelper {
val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
OriginManager(
getOrigin(
providedClientDataHash = clientDataHash,
callingAppInfo = callingAppInfo,
assets = assetManager,
relyingParty = requestOptions.rpId
).getOriginAtUsage(
relyingParty = requestOptions.rpId,
onOriginRetrieved = { appOrigin, clientDataHash ->
result.invoke(
PublicKeyCredentialUsageParameters(
@@ -466,7 +523,7 @@ object PasskeyHelper {
)
)
},
onOriginCreated = { appOrigin ->
onOriginNotRetrieved = { appOrigin, androidOriginString ->
// By default we crate an usage parameter with Android origin
result.invoke(
PublicKeyCredentialUsageParameters(
@@ -474,7 +531,7 @@ object PasskeyHelper {
clientDataResponse = ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET,
challenge = requestOptions.challenge,
origin = appOrigin.toAppOrigin()
origin = androidOriginString
),
appOrigin = appOrigin
)
@@ -522,7 +579,7 @@ object PasskeyHelper {
return if (appToCheck.verified) {
usageParameters.clientDataResponse
} else {
appOrigin.checkAppOrigin(appToCheck)?.let { origin ->
appToCheck.checkAppOrigin(appOrigin)?.let { origin ->
// Origin checked by Android app signature
ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET,

View File

@@ -20,13 +20,16 @@
package com.kunzisoft.keepass.model
import android.os.Parcelable
import android.util.Log
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64
import com.kunzisoft.keepass.model.WebOrigin.Companion.RELYING_PARTY_DEFAULT_PROTOCOL
import kotlinx.parcelize.Parcelize
@Parcelize
data class AppOrigin(
val verified: Boolean,
val androidOrigins: MutableList<AndroidOrigin> = mutableListOf(),
val webOrigins: MutableList<WebOrigin> = mutableListOf()
val webOrigins: MutableList<WebOrigin> = mutableListOf(),
) : Parcelable {
fun addAndroidOrigin(androidOrigin: AndroidOrigin) {
@@ -37,22 +40,21 @@ data class AppOrigin(
this.webOrigins.add(webOrigin)
}
val verified: Boolean
get() = androidOrigins.any { it.verified }
/**
* Verify the app origin by comparing it to the list of android origins,
* return the first verified origin or null if none is found
*/
fun checkAppOrigin(appToCheck: AppOrigin): String? {
fun checkAppOrigin(compare: AppOrigin): String? {
return androidOrigins.firstOrNull { androidOrigin ->
appToCheck.androidOrigins.any {
compare.androidOrigins.any {
it.packageName == androidOrigin.packageName
&& it.fingerprint == androidOrigin.fingerprint
}
}?.let {
AndroidOrigin(it.packageName, it.fingerprint, true)
.toAndroidOrigin()
AndroidOrigin(
packageName = it.packageName,
fingerprint = it.fingerprint
).toAndroidOrigin()
}
}
@@ -65,11 +67,6 @@ data class AppOrigin(
return androidOrigins.isEmpty() && webOrigins.isEmpty()
}
fun toAppOrigin(): String {
return androidOrigins.firstOrNull()?.toAndroidOrigin()
?: throw SecurityException("No app origin found")
}
fun toName(): String? {
return if (androidOrigins.isNotEmpty()) {
androidOrigins.first().packageName
@@ -77,13 +74,32 @@ data class AppOrigin(
webOrigins.first().origin
} else null
}
companion object {
private val TAG = AppOrigin::class.java.simpleName
fun fromOrigin(origin: String, androidOrigin: AndroidOrigin, verified: Boolean): AppOrigin {
val appOrigin = AppOrigin(verified)
if (origin.startsWith(RELYING_PARTY_DEFAULT_PROTOCOL)) {
appOrigin.apply {
addWebOrigin(WebOrigin(origin))
}
} else {
Log.w(TAG, "Unknown verified origin $origin")
appOrigin.apply {
addAndroidOrigin(androidOrigin)
}
}
return appOrigin
}
}
}
@Parcelize
data class AndroidOrigin(
val packageName: String,
val fingerprint: String?,
val verified: Boolean = true
val fingerprint: String?
) : Parcelable {
/**
@@ -107,8 +123,7 @@ data class AndroidOrigin(
@Parcelize
data class WebOrigin(
val origin: String,
val verified: Boolean = true,
val origin: String
) : Parcelable {
fun toWebOrigin(): String {
@@ -121,9 +136,8 @@ data class WebOrigin(
companion object {
const val RELYING_PARTY_DEFAULT_PROTOCOL = "https"
fun fromRelyingParty(relyingParty: String, verified: Boolean): WebOrigin = WebOrigin(
origin ="$RELYING_PARTY_DEFAULT_PROTOCOL://$relyingParty",
verified = verified
fun fromRelyingParty(relyingParty: String): WebOrigin = WebOrigin(
origin ="$RELYING_PARTY_DEFAULT_PROTOCOL://$relyingParty"
)
}
}

View File

@@ -33,7 +33,7 @@ object AppOriginEntryField {
* Parse fields of an entry to retrieve a an AppOrigin
*/
fun parseFields(getField: (id: String) -> String?): AppOrigin {
val appOrigin = AppOrigin()
val appOrigin = AppOrigin(verified = true)
// Get Application identifiers
generateSequence(0) { it + 1 }
.map { position ->
@@ -141,8 +141,7 @@ object AppOriginEntryField {
setApplicationId(appIdentifier.packageName, appIdentifier.fingerprint)
}
appOrigin?.webOrigins?.forEach { webOrigin ->
if (webOrigin.verified)
setWebDomain(webOrigin.origin, null, customFieldsAllowed)
setWebDomain(webOrigin.origin, null, customFieldsAllowed)
}
}
}