mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Move application signature function
This commit is contained in:
@@ -19,20 +19,15 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
package com.kunzisoft.keepass.credentialprovider.passkey.util
|
||||||
|
|
||||||
import android.content.pm.Signature
|
|
||||||
import android.content.pm.SigningInfo
|
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.credentials.provider.CallingAppInfo
|
import androidx.credentials.provider.CallingAppInfo
|
||||||
|
import com.kunzisoft.encrypt.HashManager.getApplicationSignatures
|
||||||
import com.kunzisoft.keepass.model.OriginApp
|
import com.kunzisoft.keepass.model.OriginApp
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
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)
|
@RequiresApi(Build.VERSION_CODES.P)
|
||||||
class OriginManager(
|
class OriginManager(
|
||||||
@@ -102,7 +97,7 @@ class OriginManager(
|
|||||||
onOriginNotRetrieved(
|
onOriginNotRetrieved(
|
||||||
OriginApp(
|
OriginApp(
|
||||||
appId = callingAppInfo.packageName,
|
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.
|
* Builds an Android Origin from a package name.
|
||||||
*/
|
*/
|
||||||
@@ -194,7 +118,5 @@ class OriginManager(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = OriginManager::class.simpleName
|
private val TAG = OriginManager::class.simpleName
|
||||||
|
|
||||||
private const val SIGNATURE_DELIMITER = "##SIG##"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,12 @@
|
|||||||
package com.kunzisoft.asymmetric
|
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 org.junit.Test
|
||||||
import java.io.File
|
|
||||||
import java.io.FileWriter
|
|
||||||
import kotlin.io.path.Path
|
|
||||||
|
|
||||||
class SignatureTest {
|
class SignatureTest {
|
||||||
|
|
||||||
// All private keys are for testing only.
|
// All private keys are for testing only.
|
||||||
// DO NOT USE THEM
|
// DO NOT USE THEM
|
||||||
|
|
||||||
|
|
||||||
// region ES256
|
// region ES256
|
||||||
private val es256PemInKeePassXC =
|
private val es256PemInKeePassXC =
|
||||||
"""
|
"""
|
||||||
@@ -159,9 +153,5 @@ class SignatureTest {
|
|||||||
assert(privateKeyPem.contains("-----BEGIN EC PRIVATE KEY-----", true).not())
|
assert(privateKeyPem.contains("-----BEGIN EC PRIVATE KEY-----", true).not())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,11 @@
|
|||||||
*/
|
*/
|
||||||
package com.kunzisoft.encrypt
|
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.ChaCha7539Engine
|
||||||
import org.bouncycastle.crypto.engines.Salsa20Engine
|
import org.bouncycastle.crypto.engines.Salsa20Engine
|
||||||
import org.bouncycastle.crypto.params.KeyParameter
|
import org.bouncycastle.crypto.params.KeyParameter
|
||||||
@@ -26,9 +31,14 @@ import org.bouncycastle.crypto.params.ParametersWithIV
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.NoSuchAlgorithmException
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.security.cert.CertificateFactory
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
object HashManager {
|
object HashManager {
|
||||||
|
|
||||||
|
private val TAG = HashManager::class.simpleName
|
||||||
|
|
||||||
fun getHash256(): MessageDigest {
|
fun getHash256(): MessageDigest {
|
||||||
val messageDigest: MessageDigest
|
val messageDigest: MessageDigest
|
||||||
try {
|
try {
|
||||||
@@ -107,4 +117,77 @@ object HashManager {
|
|||||||
|
|
||||||
return StreamCipher(cipher)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user