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() finish()
}) { }) {
val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo() val searchInfo = intent.retrieveSearchInfo() ?: SearchInfo()
val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin() val appOrigin = intent.retrieveAppOrigin() ?: AppOrigin(verified = false)
val nodeId = intent.retrieveNodeId() val nodeId = intent.retrieveNodeId()
checkSecurity(intent, nodeId) checkSecurity(intent, nodeId)
when (mSpecialMode) { 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.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.CallingAppInfo
import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderCreateCredentialRequest import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.provider.ProviderGetCredentialRequest import androidx.credentials.provider.ProviderGetCredentialRequest
import com.kunzisoft.asymmetric.Signature import com.kunzisoft.asymmetric.Signature
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode 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.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor 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.PublicKeyCredentialCreationParameters
import com.kunzisoft.keepass.credentialprovider.passkey.data.PublicKeyCredentialRequestOptions 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.model.AndroidOrigin
import com.kunzisoft.keepass.model.AppOrigin import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.WebOrigin
import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.random.KeePassDXRandom import com.kunzisoft.random.KeePassDXRandom
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.security.KeyStore import java.security.KeyStore
import java.security.MessageDigest import java.security.MessageDigest
import java.time.Instant import java.time.Instant
@@ -320,6 +326,59 @@ object PasskeyHelper {
return request.credentialOptions[0] as GetPublicKeyCredentialOption 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 * Utility method to create a passkey and the associated creation request parameters
* [intent] allows to retrieve the request * [intent] allows to retrieve the request
@@ -360,12 +419,11 @@ object PasskeyHelper {
) )
// create new entry in database // create new entry in database
OriginManager( getOrigin(
providedClientDataHash = clientDataHash, providedClientDataHash = clientDataHash,
callingAppInfo = callingAppInfo, callingAppInfo = callingAppInfo,
assets = assetManager, assets = assetManager,
relyingParty = relyingParty relyingParty = relyingParty,
).getOriginAtCreation(
onOriginRetrieved = { appInfoToStore, clientDataHash -> onOriginRetrieved = { appInfoToStore, clientDataHash ->
passkeyCreated.invoke( passkeyCreated.invoke(
passkey, passkey,
@@ -378,7 +436,7 @@ object PasskeyHelper {
) )
) )
}, },
onOriginCreated = { appInfoToStore, origin -> onOriginNotRetrieved = { appInfoToStore, origin ->
passkeyCreated.invoke( passkeyCreated.invoke(
passkey, passkey,
appInfoToStore, appInfoToStore,
@@ -451,12 +509,11 @@ object PasskeyHelper {
val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson) val requestOptions = PublicKeyCredentialRequestOptions(credentialOption.requestJson)
OriginManager( getOrigin(
providedClientDataHash = clientDataHash, providedClientDataHash = clientDataHash,
callingAppInfo = callingAppInfo, callingAppInfo = callingAppInfo,
assets = assetManager, assets = assetManager,
relyingParty = requestOptions.rpId relyingParty = requestOptions.rpId,
).getOriginAtUsage(
onOriginRetrieved = { appOrigin, clientDataHash -> onOriginRetrieved = { appOrigin, clientDataHash ->
result.invoke( result.invoke(
PublicKeyCredentialUsageParameters( PublicKeyCredentialUsageParameters(
@@ -466,7 +523,7 @@ object PasskeyHelper {
) )
) )
}, },
onOriginCreated = { appOrigin -> onOriginNotRetrieved = { appOrigin, androidOriginString ->
// By default we crate an usage parameter with Android origin // By default we crate an usage parameter with Android origin
result.invoke( result.invoke(
PublicKeyCredentialUsageParameters( PublicKeyCredentialUsageParameters(
@@ -474,7 +531,7 @@ object PasskeyHelper {
clientDataResponse = ClientDataBuildResponse( clientDataResponse = ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET, type = ClientDataBuildResponse.Type.GET,
challenge = requestOptions.challenge, challenge = requestOptions.challenge,
origin = appOrigin.toAppOrigin() origin = androidOriginString
), ),
appOrigin = appOrigin appOrigin = appOrigin
) )
@@ -522,7 +579,7 @@ object PasskeyHelper {
return if (appToCheck.verified) { return if (appToCheck.verified) {
usageParameters.clientDataResponse usageParameters.clientDataResponse
} else { } else {
appOrigin.checkAppOrigin(appToCheck)?.let { origin -> appToCheck.checkAppOrigin(appOrigin)?.let { origin ->
// Origin checked by Android app signature // Origin checked by Android app signature
ClientDataBuildResponse( ClientDataBuildResponse(
type = ClientDataBuildResponse.Type.GET, type = ClientDataBuildResponse.Type.GET,

View File

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

View File

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