From 200881278cf1808b8756e3f0a8bb5138fdd6cbd9 Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Mon, 1 Sep 2025 14:48:36 +0200 Subject: [PATCH] fix: Change Android origin --- .../activity/PasskeyLauncherActivity.kt | 3 +- .../data/AuthenticatorAssertionResponse.kt | 2 +- .../data/AuthenticatorAttestationResponse.kt | 2 +- .../passkey/data/ClientDataBuildResponse.kt | 2 +- .../PublicKeyCredentialCreationOptions.kt | 2 +- .../data/PublicKeyCredentialRequestOptions.kt | 2 +- .../PublicKeyCredentialUsageParameters.kt | 6 +- .../passkey/util/Base64Helper.kt | 42 --------- .../passkey/util/OriginManager.kt | 57 ++++++------ .../passkey/util/PasskeyHelper.kt | 46 +++------- .../com/kunzisoft/encrypt/Base64Helper.kt | 23 +++++ .../java/com/kunzisoft/encrypt/HashManager.kt | 48 +++++++++- .../com/kunzisoft/keepass/model/AppOrigin.kt | 91 +++++++++---------- .../keepass/model/AppOriginEntryField.kt | 4 +- 14 files changed, 158 insertions(+), 172 deletions(-) delete mode 100644 app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/Base64Helper.kt create mode 100644 crypto/src/main/java/com/kunzisoft/encrypt/Base64Helper.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 d04a36075..58be36424 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 @@ -234,8 +234,7 @@ class PasskeyLauncherActivity : DatabaseModeActivity() { Log.d(TAG, "Launch passkey selection") retrievePasskeyUsageRequestParameters( intent = intent, - assetManager = assets, - appOrigin = appOrigin, + assetManager = assets ) { usageParameters -> // Save the requested parameters mUsageParameters = usageParameters diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt index 7ea51bced..74df510bb 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAssertionResponse.kt @@ -21,7 +21,7 @@ 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 com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode import org.json.JSONObject class AuthenticatorAssertionResponse( diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt index 732c7191b..07ac0ba45 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/AuthenticatorAttestationResponse.kt @@ -19,7 +19,7 @@ */ package com.kunzisoft.keepass.credentialprovider.passkey.data -import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode +import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode import com.kunzisoft.keepass.utils.UUIDUtils.asBytes import org.json.JSONArray import org.json.JSONObject diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt index 8427b7b68..0e9454d19 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/ClientDataBuildResponse.kt @@ -20,7 +20,7 @@ package com.kunzisoft.keepass.credentialprovider.passkey.data import com.kunzisoft.encrypt.HashManager -import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode +import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode import org.json.JSONObject open class ClientDataBuildResponse( diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt index 66fa14152..80d9ab2b6 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialCreationOptions.kt @@ -20,7 +20,7 @@ package com.kunzisoft.keepass.credentialprovider.passkey.data import android.util.Log -import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper +import com.kunzisoft.encrypt.Base64Helper import org.json.JSONObject class PublicKeyCredentialCreationOptions( diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt index a2cf97906..42b0d75de 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialRequestOptions.kt @@ -19,7 +19,7 @@ */ package com.kunzisoft.keepass.credentialprovider.passkey.data -import com.kunzisoft.keepass.credentialprovider.passkey.util.Base64Helper +import com.kunzisoft.encrypt.Base64Helper import org.json.JSONObject class PublicKeyCredentialRequestOptions(requestJson: String) { diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt index 9bb0f80fb..c59541ad7 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/data/PublicKeyCredentialUsageParameters.kt @@ -19,12 +19,10 @@ */ package com.kunzisoft.keepass.credentialprovider.passkey.data -import com.kunzisoft.keepass.model.AndroidOrigin -import com.kunzisoft.keepass.model.WebOrigin +import com.kunzisoft.keepass.model.AppOrigin data class PublicKeyCredentialUsageParameters( val publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions, val clientDataResponse: ClientDataResponse, - val androidOrigin: AndroidOrigin, - val webOrigin: WebOrigin, + val appOrigin: AppOrigin ) \ No newline at end of file diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/Base64Helper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/Base64Helper.kt deleted file mode 100644 index faebbfa40..000000000 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/Base64Helper.kt +++ /dev/null @@ -1,42 +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.util.Base64 - -class Base64Helper { - - companion object { - - fun b64Decode(encodedString: String): ByteArray { - return Base64.decode( - encodedString, - Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE - ) - } - - fun b64Encode(data: ByteArray): String { - return Base64.encodeToString( - data, - Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE - ) - } - } -} \ No newline at end of file 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 index 01f94e60d..252e4c69a 100644 --- 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 @@ -24,10 +24,9 @@ import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import androidx.credentials.provider.CallingAppInfo -import com.kunzisoft.encrypt.HashManager.getApplicationSignatures +import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints import com.kunzisoft.keepass.model.AndroidOrigin import com.kunzisoft.keepass.model.AppOrigin -import com.kunzisoft.keepass.model.Verification import com.kunzisoft.keepass.model.WebOrigin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -81,27 +80,26 @@ class OriginManager( * calls [onOriginCreated] if the origin was created manually, origin is verified if present in the KeePass database */ suspend fun getOriginAtUsage( - appOrigin: AppOrigin, - onOriginRetrieved: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin, clientDataHash: ByteArray) -> Unit, - onOriginCreated: (androidOrigin: AndroidOrigin, webOrigin: WebOrigin) -> Unit + onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit, + onOriginCreated: (appOrigin: AppOrigin) -> Unit ) { getOrigin( onOriginRetrieved = { androidOrigin, webOrigin, origin, clientDataHash -> - onOriginRetrieved(androidOrigin, webOrigin, clientDataHash) + onOriginRetrieved( + AppOrigin().apply { + addAndroidOrigin(androidOrigin) + addWebOrigin(webOrigin) + }, + clientDataHash + ) }, - onOriginNotRetrieved = { appIdentifierToCheck, webOrigin -> + onOriginNotRetrieved = { androidOrigin, webOrigin -> // Check the app signature in the appOrigin, webOrigin cannot be checked now onOriginCreated( - AndroidOrigin( - packageName = appIdentifierToCheck.packageName, - signature = appIdentifierToCheck.signature, - verification = - if (appOrigin.containsVerifiedAndroidOrigin(appIdentifierToCheck)) - Verification.MANUALLY_VERIFIED - else - Verification.NOT_VERIFIED - ), - webOrigin + AppOrigin().apply { + addAndroidOrigin(androidOrigin) + addWebOrigin(webOrigin) + } ) } ) @@ -129,23 +127,27 @@ class OriginManager( callOrigin = callingAppInfo.getOrigin(privilegedAllowlist)?.removeSuffix("/") val androidOrigin = AndroidOrigin( packageName = callingAppInfo.packageName, - signature = callingAppInfo.signingInfo - .getApplicationSignatures(), - verification = Verification.NOT_VERIFIED + 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, - signature = androidOrigin.signature, - verification = Verification.AUTOMATICALLY_VERIFIED + fingerprint = androidOrigin.fingerprint, + verified = true ), - WebOrigin.fromRelyingParty( - relyingParty = relyingParty, - verification = Verification.AUTOMATICALLY_VERIFIED + WebOrigin( + origin = webOrigin.origin, + verified = true ), callOrigin, providedClientDataHash @@ -153,10 +155,7 @@ class OriginManager( } else { onOriginNotRetrieved( androidOrigin, - WebOrigin.fromRelyingParty( - relyingParty = relyingParty, - verification = Verification.NOT_VERIFIED - ) + webOrigin ) } } 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 8b4350886..9691462d1 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 @@ -39,6 +39,7 @@ 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.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor @@ -50,12 +51,10 @@ 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.credentialprovider.passkey.util.Base64Helper.Companion.b64Encode 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.Verification import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.random.KeePassDXRandom @@ -436,13 +435,11 @@ object PasskeyHelper { * Utility method to use a passkey and create the associated usage request parameters * [intent] allows to retrieve the request * [assetManager] has been transferred to the origin manager to manage package verification files - * [appOrigin] retrieves the origin params stored in an entry, which may be null if not found or if the database is closed. * [result] is called asynchronously after the creation of PublicKeyCredentialUsageParameters, the origin associated with it may or may not be verified */ suspend fun retrievePasskeyUsageRequestParameters( intent: Intent, assetManager: AssetManager, - appOrigin: AppOrigin, result: (PublicKeyCredentialUsageParameters) -> Unit ) { val getCredentialRequest = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) @@ -460,18 +457,16 @@ object PasskeyHelper { assets = assetManager, relyingParty = requestOptions.rpId ).getOriginAtUsage( - appOrigin = appOrigin, - onOriginRetrieved = { androidOrigin, webOrigin, clientDataHash -> + onOriginRetrieved = { appOrigin, clientDataHash -> result.invoke( PublicKeyCredentialUsageParameters( publicKeyCredentialRequestOptions = requestOptions, clientDataResponse = ClientDataDefinedResponse(clientDataHash), - androidOrigin = androidOrigin, - webOrigin = webOrigin + appOrigin = appOrigin ) ) }, - onOriginCreated = { androidOrigin, webOrigin -> + onOriginCreated = { appOrigin -> // By default we crate an usage parameter with Android origin result.invoke( PublicKeyCredentialUsageParameters( @@ -479,10 +474,9 @@ object PasskeyHelper { clientDataResponse = ClientDataBuildResponse( type = ClientDataBuildResponse.Type.GET, challenge = requestOptions.challenge, - origin = androidOrigin.toAndroidOrigin() + origin = appOrigin.toAppOrigin() ), - androidOrigin = androidOrigin, - webOrigin = webOrigin + appOrigin = appOrigin ) ) } @@ -519,37 +513,23 @@ object PasskeyHelper { /** * Verify that the application signature is contained in the [appOrigin] - * or that the webDomain contains the origin */ fun getVerifiedGETClientDataResponse( usageParameters: PublicKeyCredentialUsageParameters, appOrigin: AppOrigin ): ClientDataResponse { - val appToCheck = usageParameters.androidOrigin - val webToCheck = usageParameters.webOrigin - if (appToCheck.verification == Verification.AUTOMATICALLY_VERIFIED) { - return usageParameters.clientDataResponse + val appToCheck = usageParameters.appOrigin + return if (appToCheck.verified) { + usageParameters.clientDataResponse } else { - if (appOrigin.containsVerifiedAndroidOrigin(appToCheck)) { - if (webToCheck.verification.verified - || appOrigin.containsVerifiedWebOrigin(webToCheck)) { - // Origin checked by URL - return ClientDataBuildResponse( - type = ClientDataBuildResponse.Type.GET, - challenge = usageParameters.publicKeyCredentialRequestOptions.challenge, - origin = webToCheck.toWebOrigin() - ) - } + appOrigin.checkAppOrigin(appToCheck)?.let { origin -> // Origin checked by Android app signature - return ClientDataBuildResponse( + ClientDataBuildResponse( type = ClientDataBuildResponse.Type.GET, challenge = usageParameters.publicKeyCredentialRequestOptions.challenge, - origin = appOrigin.firstVerifiedWebOrigin()?.toWebOrigin() - ?: appToCheck.toAndroidOrigin() + origin = origin ) - } else { - throw SecurityException("Wrong signature for ${appToCheck.packageName}") - } + } ?: throw SecurityException("Wrong signature for $appToCheck") } } } \ No newline at end of file diff --git a/crypto/src/main/java/com/kunzisoft/encrypt/Base64Helper.kt b/crypto/src/main/java/com/kunzisoft/encrypt/Base64Helper.kt new file mode 100644 index 000000000..c25e251b6 --- /dev/null +++ b/crypto/src/main/java/com/kunzisoft/encrypt/Base64Helper.kt @@ -0,0 +1,23 @@ +package com.kunzisoft.encrypt + +import android.util.Base64 + +class Base64Helper { + + companion object { + + fun b64Decode(encodedString: String): ByteArray { + return Base64.decode( + encodedString, + Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE + ) + } + + fun b64Encode(data: ByteArray): String { + return Base64.encodeToString( + data, + Base64.NO_PADDING or Base64.NO_WRAP or Base64.URL_SAFE + ) + } + } +} \ No newline at end of file diff --git a/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt b/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt index 541040907..8930352e5 100644 --- a/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt +++ b/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt @@ -118,7 +118,7 @@ object HashManager { return StreamCipher(cipher) } - private const val SIGNATURE_DELIMITER = "##SIG##" + const val SIGNATURE_DELIMITER = "##SIG##" /** * Converts a Signature object into its SHA-256 fingerprint string. @@ -149,7 +149,7 @@ object HashManager { * @param signingInfo The SigningInfo object to retrieve the strings signatures * @return A List of SHA-256 fingerprint strings, or null if an error occurs or no signatures are found. */ - fun getAllSignatures(signingInfo: SigningInfo?): List? { + fun getAllFingerprints(signingInfo: SigningInfo?): List? { try { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) throw AndroidException("API level ${Build.VERSION.SDK_INT} not supported") @@ -181,13 +181,51 @@ object HashManager { /** * Combines a list of signature into a single string for database storage. * - * @return A single string with fingerprints joined by a delimiter, or null if the input list is null or empty. + * @return A single string with fingerprints joined by a ##SIG## delimiter, + * or null if the input list is null or empty. */ - fun SigningInfo.getApplicationSignatures(): String? { - val fingerprints = getAllSignatures(this) + fun SigningInfo.getApplicationFingerprints(): String? { + val fingerprints = getAllFingerprints(this) if (fingerprints.isNullOrEmpty()) { return null } return fingerprints.joinToString(SIGNATURE_DELIMITER) } + + /** + * Transforms a colon-separated hex fingerprint string into a URL-safe, + * padding-removed Base64 string, mimicking the Python behavior: + * base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', '') + * + * Only check the first footprint if there are several delimited by ##SIG##. + * + * @param fingerprint The colon-separated hex fingerprint string (e.g., "91:F7:CB:..."). + * @return The Android App Origin string. + * @throws IllegalArgumentException if the hex string (after removing colons) has an odd length + * or contains non-hex characters. + */ + fun fingerprintToUrlSafeBase64(fingerprint: String): String { + val firstFingerprint = fingerprint.split(SIGNATURE_DELIMITER).firstOrNull()?.trim() + if (firstFingerprint.isNullOrEmpty()) { + throw IllegalArgumentException("Invalid fingerprint $fingerprint") + } + val hexStringNoColons = fingerprint.replace(":", "") + if (hexStringNoColons.length % 2 != 0) { + throw IllegalArgumentException("Hex string must have an even number of characters: $hexStringNoColons") + } + if (hexStringNoColons.length != 64) { + throw IllegalArgumentException("Expected a 64-character hex string for a SHA-256 hash, but got ${hexStringNoColons.length} characters.") + } + val hashBytes = ByteArray(hexStringNoColons.length / 2) + for (i in hashBytes.indices) { + try { + val index = i * 2 + val byteValue = hexStringNoColons.substring(index, index + 2).toInt(16) + hashBytes[i] = byteValue.toByte() + } catch (e: NumberFormatException) { + throw IllegalArgumentException("Invalid hex character in fingerprint: $hexStringNoColons", e) + } + } + return Base64Helper.b64Encode(hashBytes) + } } 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 98838b763..f40635734 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt @@ -20,6 +20,7 @@ package com.kunzisoft.keepass.model import android.os.Parcelable +import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64 import kotlinx.parcelize.Parcelize @Parcelize @@ -36,44 +37,22 @@ data class AppOrigin( this.webOrigins.add(webOrigin) } - fun containsVerifiedAndroidOrigin(androidOrigin: AndroidOrigin): Boolean { - return androidOrigins.any { - it.packageName == androidOrigin.packageName - && it.signature == androidOrigin.signature - && it.verification.verified - } - } + val verified: Boolean + get() = androidOrigins.any { it.verified } - fun getFirstAndroidOrigin(): AndroidOrigin? { - return androidOrigins.firstOrNull() - } - - fun containsVerifiedWebOrigin(webOrigin: WebOrigin): Boolean { - return this.webOrigins.any { - it.origin == webOrigin.origin - && it.verification.verified - } - } - - fun containsUnverifiedWebOrigin(): Boolean { - return this.webOrigins.any { - it.verification.verified.not() - } - } - - fun firstVerifiedWebOrigin(): WebOrigin? { - return webOrigins.first { - it.verification.verified - } - } - - fun getFirstWebOrigin(): WebOrigin? { - return webOrigins.firstOrNull() - } - - fun firstUnverifiedOrigin(): WebOrigin? { - return webOrigins.first { - it.verification.verified.not() + /** + * 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? { + return androidOrigins.firstOrNull { androidOrigin -> + appToCheck.androidOrigins.any { + it.packageName == androidOrigin.packageName + && it.fingerprint == androidOrigin.fingerprint + } + }?.let { + AndroidOrigin(it.packageName, it.fingerprint, true) + .toAndroidOrigin() } } @@ -86,6 +65,11 @@ 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 @@ -95,29 +79,36 @@ data class AppOrigin( } } -enum class Verification { - MANUALLY_VERIFIED, AUTOMATICALLY_VERIFIED, NOT_VERIFIED; - - val verified: Boolean - get() = this == MANUALLY_VERIFIED || this == AUTOMATICALLY_VERIFIED -} - @Parcelize data class AndroidOrigin( val packageName: String, - val signature: String? = null, - val verification: Verification = Verification.AUTOMATICALLY_VERIFIED, + val fingerprint: String?, + val verified: Boolean = true ) : Parcelable { + /** + * Creates an Android App Origin string of the form "android:apk-key-hash:" + * from a colon-separated hex fingerprint string. + * + * The input fingerprint is assumed to be the SHA-256 hash of the app's signing certificate. + * + * @param fingerprint The colon-separated hex fingerprint string (e.g., "91:F7:CB:..."). + * @return The Android App Origin string. + * @throws IllegalArgumentException if the hex string (after removing colons) has an odd length + * or contains non-hex characters. + */ fun toAndroidOrigin(): String { - return "android:apk-key-hash:${packageName}" + if (fingerprint == null) { + throw IllegalArgumentException("Fingerprint $fingerprint cannot be null") + } + return "android:apk-key-hash:${fingerprintToUrlSafeBase64(fingerprint)}" } } @Parcelize data class WebOrigin( val origin: String, - val verification: Verification = Verification.AUTOMATICALLY_VERIFIED, + val verified: Boolean = true, ) : Parcelable { fun toWebOrigin(): String { @@ -130,9 +121,9 @@ data class WebOrigin( companion object { const val RELYING_PARTY_DEFAULT_PROTOCOL = "https" - fun fromRelyingParty(relyingParty: String, verification: Verification): WebOrigin = WebOrigin( + fun fromRelyingParty(relyingParty: String, verified: Boolean): WebOrigin = WebOrigin( origin ="$RELYING_PARTY_DEFAULT_PROTOCOL://$relyingParty", - verification = verification + verified = verified ) } } \ 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 d7c84a4ac..6b36ad365 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/AppOriginEntryField.kt @@ -138,10 +138,10 @@ object AppOriginEntryField { */ fun EntryInfo.setAppOrigin(appOrigin: AppOrigin?, customFieldsAllowed: Boolean) { appOrigin?.androidOrigins?.forEach { appIdentifier -> - setApplicationId(appIdentifier.packageName, appIdentifier.signature) + setApplicationId(appIdentifier.packageName, appIdentifier.fingerprint) } appOrigin?.webOrigins?.forEach { webOrigin -> - if (webOrigin.verification.verified) + if (webOrigin.verified) setWebDomain(webOrigin.origin, null, customFieldsAllowed) } }