From 7e41527cfe4423262f7d21995cf72a8bfd2cb295 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Mon, 1 Sep 2025 15:29:19 +0200 Subject: [PATCH] fix: Refactoring verification --- .../activity/PasskeyLauncherActivity.kt | 2 +- .../passkey/util/OriginManager.kt | 168 ------------------ .../passkey/util/PasskeyHelper.kt | 77 ++++++-- .../com/kunzisoft/keepass/model/AppOrigin.kt | 54 +++--- .../keepass/model/AppOriginEntryField.kt | 5 +- 5 files changed, 104 insertions(+), 202 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt index 58be36424..7239038dc 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/activity/PasskeyLauncherActivity.kt @@ -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) { diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt deleted file mode 100644 index 252e4c69a..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/OriginManager.kt +++ /dev/null @@ -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 . - * - */ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt index 9691462d1..27965bed1 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt @@ -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, diff --git a/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt b/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt index f40635734..a46ee1a63 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt @@ -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 = mutableListOf(), - val webOrigins: MutableList = mutableListOf() + val webOrigins: MutableList = 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" ) } } \ No newline at end of file diff --git a/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt b/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt index 6b36ad365..e5122af42 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt @@ -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) } } } \ No newline at end of file