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)
}
}