fix: Move application signature function

This commit is contained in:
J-Jamet
2025-08-27 19:14:24 +02:00
parent 5bd866e104
commit fcf723849b
3 changed files with 85 additions and 90 deletions

View File

@@ -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<String>? {
try {
val signatures = mutableSetOf<String>()
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##"
}
}

View File

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

View File

@@ -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<String>? {
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
throw AndroidException("API level ${Build.VERSION.SDK_INT} not supported")
val signatures = mutableSetOf<String>()
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)
}
}