From fcf723849b414e0d2a104b0c8f42558085fdf54e Mon Sep 17 00:00:00 2001 From: J-Jamet Date: Wed, 27 Aug 2025 19:14:24 +0200 Subject: [PATCH] fix: Move application signature function --- .../passkey/util/OriginManager.kt | 82 +----------------- .../com/kunzisoft/asymmetric/SignatureTest.kt | 10 --- .../java/com/kunzisoft/encrypt/HashManager.kt | 83 +++++++++++++++++++ 3 files changed, 85 insertions(+), 90 deletions(-) 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 b36db0c6e..b05db330b 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 @@ -19,20 +19,15 @@ */ package com.kunzisoft.keepass.credentialprovider.passkey.util -import android.content.pm.Signature -import android.content.pm.SigningInfo 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.getApplicationSignatures import com.kunzisoft.keepass.model.OriginApp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.security.MessageDigest -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.Locale @RequiresApi(Build.VERSION_CODES.P) class OriginManager( @@ -102,7 +97,7 @@ class OriginManager( onOriginNotRetrieved( OriginApp( appId = callingAppInfo.packageName, - appSignature = getApplicationSignatures(callingAppInfo.signingInfo) + appSignature = callingAppInfo.signingInfo.getApplicationSignatures() ) ) } @@ -110,77 +105,6 @@ class OriginManager( } } - // TODO Move in Crypto package and make unit tests - /** - * Converts a Signature object into its SHA-256 fingerprint string. - * The fingerprint is typically represented as uppercase hex characters separated by colons. - */ - private fun signatureToSha256Fingerprint(signature: Signature): String? { - return try { - val certificateFactory = CertificateFactory.getInstance("X.509") - val x509Certificate = certificateFactory.generateCertificate( - signature.toByteArray().inputStream() - ) as X509Certificate - - val messageDigest = MessageDigest.getInstance("SHA-256") - val digest = messageDigest.digest(x509Certificate.encoded) - - // Format as colon-separated HEX uppercase string - digest.joinToString(separator = ":") { byte -> "%02X".format(byte) } - .uppercase(Locale.US) - } catch (e: Exception) { - Log.e("SigningInfoUtil", "Error converting signature to SHA-256 fingerprint", e) - null - } - } - - /** - * Retrieves all relevant SHA-256 signature fingerprints for a given package. - * - * @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? { - try { - val signatures = mutableSetOf() - if (signingInfo != null) { - // Includes past and current keys if rotation occurred. This is generally preferred. - signingInfo.signingCertificateHistory?.forEach { signature -> - signatureToSha256Fingerprint(signature)?.let { signatures.add(it) } - } - // If only one signer and history is empty (e.g. new app), this might be needed. - // Or if multiple signers are explicitly used for the APK content. - if (signingInfo.hasMultipleSigners()) { - signingInfo.apkContentsSigners?.forEach { signature -> - signatureToSha256Fingerprint(signature)?.let { signatures.add(it) } - } - } else { // Fallback for single signer if history was somehow null/empty - signingInfo.signingCertificateHistory?.firstOrNull()?.let { - signatureToSha256Fingerprint(it)?.let { fp -> signatures.add(fp) } - } - } - } - return if (signatures.isEmpty()) null else signatures.toList() - } catch (e: Exception) { - Log.e(TAG, "Error getting signatures", e) - return null - } - } - - - /** - * 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. - */ - private fun getApplicationSignatures(signingInfo: SigningInfo?): String? { - val fingerprints = getAllSignatures(signingInfo) - if (fingerprints.isNullOrEmpty()) { - return null - } - return fingerprints.joinToString(SIGNATURE_DELIMITER) - } - /** * Builds an Android Origin from a package name. */ @@ -194,7 +118,5 @@ class OriginManager( companion object { private val TAG = OriginManager::class.simpleName - - private const val SIGNATURE_DELIMITER = "##SIG##" } } \ No newline at end of file diff --git a/crypto/src/androidTest/java/com/kunzisoft/asymmetric/SignatureTest.kt b/crypto/src/androidTest/java/com/kunzisoft/asymmetric/SignatureTest.kt index c6e3a3d17..bb5157aa2 100644 --- a/crypto/src/androidTest/java/com/kunzisoft/asymmetric/SignatureTest.kt +++ b/crypto/src/androidTest/java/com/kunzisoft/asymmetric/SignatureTest.kt @@ -1,18 +1,12 @@ package com.kunzisoft.asymmetric -import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPrivateKey -import org.bouncycastle.jcajce.provider.asymmetric.edec.BCXDHPrivateKey import org.junit.Test -import java.io.File -import java.io.FileWriter -import kotlin.io.path.Path class SignatureTest { // All private keys are for testing only. // DO NOT USE THEM - // region ES256 private val es256PemInKeePassXC = """ @@ -159,9 +153,5 @@ class SignatureTest { assert(privateKeyPem.contains("-----BEGIN EC PRIVATE KEY-----", true).not()) } - - - // endregion - } \ 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 1c7e1e573..541040907 100644 --- a/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt +++ b/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt @@ -19,6 +19,11 @@ */ package com.kunzisoft.encrypt +import android.content.pm.Signature +import android.content.pm.SigningInfo +import android.os.Build +import android.util.AndroidException +import android.util.Log import org.bouncycastle.crypto.engines.ChaCha7539Engine import org.bouncycastle.crypto.engines.Salsa20Engine import org.bouncycastle.crypto.params.KeyParameter @@ -26,9 +31,14 @@ import org.bouncycastle.crypto.params.ParametersWithIV import java.io.IOException import java.security.MessageDigest import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.Locale object HashManager { + private val TAG = HashManager::class.simpleName + fun getHash256(): MessageDigest { val messageDigest: MessageDigest try { @@ -107,4 +117,77 @@ object HashManager { return StreamCipher(cipher) } + + private const val SIGNATURE_DELIMITER = "##SIG##" + + /** + * Converts a Signature object into its SHA-256 fingerprint string. + * The fingerprint is typically represented as uppercase hex characters separated by colons. + */ + private fun signatureToSha256Fingerprint(signature: Signature): String? { + return try { + val certificateFactory = CertificateFactory.getInstance("X.509") + val x509Certificate = certificateFactory.generateCertificate( + signature.toByteArray().inputStream() + ) as X509Certificate + + val messageDigest = MessageDigest.getInstance("SHA-256") + val digest = messageDigest.digest(x509Certificate.encoded) + + // Format as colon-separated HEX uppercase string + digest.joinToString(separator = ":") { byte -> "%02X".format(byte) } + .uppercase(Locale.US) + } catch (e: Exception) { + Log.e("SigningInfoUtil", "Error converting signature to SHA-256 fingerprint", e) + null + } + } + + /** + * Retrieves all relevant SHA-256 signature fingerprints for a given package. + * + * @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? { + try { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) + throw AndroidException("API level ${Build.VERSION.SDK_INT} not supported") + val signatures = mutableSetOf() + if (signingInfo != null) { + // Includes past and current keys if rotation occurred. This is generally preferred. + signingInfo.signingCertificateHistory?.forEach { signature -> + signatureToSha256Fingerprint(signature)?.let { signatures.add(it) } + } + // If only one signer and history is empty (e.g. new app), this might be needed. + // Or if multiple signers are explicitly used for the APK content. + if (signingInfo.hasMultipleSigners()) { + signingInfo.apkContentsSigners?.forEach { signature -> + signatureToSha256Fingerprint(signature)?.let { signatures.add(it) } + } + } else { // Fallback for single signer if history was somehow null/empty + signingInfo.signingCertificateHistory?.firstOrNull()?.let { + signatureToSha256Fingerprint(it)?.let { fp -> signatures.add(fp) } + } + } + } + return if (signatures.isEmpty()) null else signatures.toList() + } catch (e: Exception) { + Log.e(TAG, "Error getting signatures", e) + return null + } + } + + /** + * 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. + */ + fun SigningInfo.getApplicationSignatures(): String? { + val fingerprints = getAllSignatures(this) + if (fingerprints.isNullOrEmpty()) { + return null + } + return fingerprints.joinToString(SIGNATURE_DELIMITER) + } }