fix: Small refactoring

This commit is contained in:
J-Jamet
2025-09-02 13:11:55 +02:00
parent a3bd5e1593
commit 437a704bc8
6 changed files with 288 additions and 296 deletions

View File

@@ -40,8 +40,8 @@ import androidx.credentials.provider.PendingIntentHandler
import androidx.credentials.provider.ProviderCreateCredentialRequest import androidx.credentials.provider.ProviderCreateCredentialRequest
import androidx.credentials.provider.ProviderGetCredentialRequest import androidx.credentials.provider.ProviderGetCredentialRequest
import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode
import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints
import com.kunzisoft.encrypt.Signature import com.kunzisoft.encrypt.Signature
import com.kunzisoft.encrypt.Signature.getApplicationFingerprints
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAssertionResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse import com.kunzisoft.keepass.credentialprovider.passkey.data.AuthenticatorAttestationResponse
import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor import com.kunzisoft.keepass.credentialprovider.passkey.data.Cbor
@@ -58,7 +58,6 @@ import com.kunzisoft.keepass.model.AppOrigin
import com.kunzisoft.keepass.model.EntryInfo import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.model.Passkey import com.kunzisoft.keepass.model.Passkey
import com.kunzisoft.keepass.model.SearchInfo import com.kunzisoft.keepass.model.SearchInfo
import com.kunzisoft.keepass.model.WebOrigin
import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.StringUtil.toHexString
import com.kunzisoft.keepass.utils.getParcelableExtraCompat import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -271,7 +270,7 @@ object PasskeyHelper {
keyStore.load(null) keyStore.load(null)
val hmacKey = try { val hmacKey = try {
keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey
} catch (e: Exception) { } catch (_: Exception) {
// key not found // key not found
generateKey() generateKey()
} }
@@ -338,7 +337,6 @@ object PasskeyHelper {
providedClientDataHash: ByteArray?, providedClientDataHash: ByteArray?,
callingAppInfo: CallingAppInfo?, callingAppInfo: CallingAppInfo?,
assets: AssetManager, assets: AssetManager,
relyingParty: String,
onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit, onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit,
onOriginNotRetrieved: (appOrigin: AppOrigin, androidOriginString: String) -> Unit onOriginNotRetrieved: (appOrigin: AppOrigin, androidOriginString: String) -> Unit
) { ) {
@@ -356,9 +354,6 @@ object PasskeyHelper {
packageName = callingAppInfo.packageName, packageName = callingAppInfo.packageName,
fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints() fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints()
) )
val webOrigin = WebOrigin.fromRelyingParty(
relyingParty = relyingParty
)
// Check if the webDomain is validated for the // Check if the webDomain is validated for the
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (callOrigin != null && providedClientDataHash != null) { if (callOrigin != null && providedClientDataHash != null) {
@@ -436,7 +431,6 @@ object PasskeyHelper {
providedClientDataHash = clientDataHash, providedClientDataHash = clientDataHash,
callingAppInfo = callingAppInfo, callingAppInfo = callingAppInfo,
assets = assetManager, assets = assetManager,
relyingParty = relyingParty,
onOriginRetrieved = { appInfoToStore, clientDataHash -> onOriginRetrieved = { appInfoToStore, clientDataHash ->
passkeyCreated.invoke( passkeyCreated.invoke(
passkey, passkey,
@@ -527,7 +521,6 @@ object PasskeyHelper {
providedClientDataHash = clientDataHash, providedClientDataHash = clientDataHash,
callingAppInfo = callingAppInfo, callingAppInfo = callingAppInfo,
assets = assetManager, assets = assetManager,
relyingParty = requestOptions.rpId,
onOriginRetrieved = { appOrigin, clientDataHash -> onOriginRetrieved = { appOrigin, clientDataHash ->
result.invoke( result.invoke(
PublicKeyCredentialUsageParameters( PublicKeyCredentialUsageParameters(

View File

@@ -24,7 +24,7 @@ import org.junit.Assert.assertArrayEquals
import org.junit.Test import org.junit.Test
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.* import java.util.Random
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.CipherInputStream import javax.crypto.CipherInputStream
import javax.crypto.CipherOutputStream import javax.crypto.CipherOutputStream

View File

@@ -1,6 +1,6 @@
package com.kunzisoft.encrypt package com.kunzisoft.encrypt
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64 import com.kunzisoft.encrypt.Signature.fingerprintToUrlSafeBase64
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test

View File

@@ -19,11 +19,6 @@
*/ */
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
@@ -31,14 +26,9 @@ 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 {
@@ -117,115 +107,4 @@ object HashManager {
return StreamCipher(cipher) return StreamCipher(cipher)
} }
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 getAllFingerprints(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 ##SIG## delimiter,
* or null if the input list is null or empty.
*/
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 = firstFingerprint.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)
}
} }

View File

@@ -1,5 +1,8 @@
package com.kunzisoft.encrypt package com.kunzisoft.encrypt
import android.content.pm.SigningInfo
import android.os.Build
import android.util.AndroidException
import android.util.Log import android.util.Log
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
@@ -17,15 +20,17 @@ import java.io.StringReader
import java.io.StringWriter import java.io.StringWriter
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyPairGenerator import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
import java.security.Security import java.security.Security
import java.security.Signature import java.security.Signature
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.ECGenParameterSpec import java.security.spec.ECGenParameterSpec
import java.util.Locale
class Signature { object Signature {
companion object {
// see at https://www.iana.org/assignments/cose/cose.xhtml // see at https://www.iana.org/assignments/cose/cose.xhtml
const val ES256_ALGORITHM: Long = -7 const val ES256_ALGORITHM: Long = -7
@@ -96,7 +101,8 @@ class Signature {
fun generateKeyPair(keyTypeIdList: List<Long>): Pair<KeyPair, Long>? { fun generateKeyPair(keyTypeIdList: List<Long>): Pair<KeyPair, Long>? {
for (typeId in keyTypeIdList) { for (typeId in keyTypeIdList) {
if (typeId == ES256_ALGORITHM) { when (typeId) {
ES256_ALGORITHM -> {
val es256CurveNameBC = "secp256r1" val es256CurveNameBC = "secp256r1"
val spec = ECGenParameterSpec(es256CurveNameBC) val spec = ECGenParameterSpec(es256CurveNameBC)
val keyPairGen = val keyPairGen =
@@ -105,20 +111,23 @@ class Signature {
val keyPair = keyPairGen.genKeyPair() val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, ES256_ALGORITHM) return Pair(keyPair, ES256_ALGORITHM)
} else if (typeId == RS256_ALGORITHM) { }
RS256_ALGORITHM -> {
val keyPairGen = val keyPairGen =
KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME) KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME)
keyPairGen.initialize(RS256_KEY_SIZE_IN_BITS) keyPairGen.initialize(RS256_KEY_SIZE_IN_BITS)
val keyPair = keyPairGen.genKeyPair() val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, RS256_ALGORITHM) return Pair(keyPair, RS256_ALGORITHM)
} else if (typeId == ED_DSA_ALGORITHM) { }
ED_DSA_ALGORITHM -> {
val keyPairGen = val keyPairGen =
KeyPairGenerator.getInstance("Ed25519", BouncyCastleProvider.PROVIDER_NAME) KeyPairGenerator.getInstance("Ed25519", BouncyCastleProvider.PROVIDER_NAME)
val keyPair = keyPairGen.genKeyPair() val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, ED_DSA_ALGORITHM) return Pair(keyPair, ED_DSA_ALGORITHM)
} }
} }
}
Log.e(this::class.java.simpleName, "generateKeyPair: no known key type id found") Log.e(this::class.java.simpleName, "generateKeyPair: no known key type id found")
return null return null
@@ -233,5 +242,116 @@ class Signature {
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found") Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")
return null return null
} }
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: android.content.pm.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 getAllFingerprints(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(Signature::class.java.simpleName, "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 ##SIG## delimiter,
* or null if the input list is null or empty.
*/
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 = firstFingerprint.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)
} }
} }

View File

@@ -21,7 +21,7 @@ package com.kunzisoft.keepass.model
import android.os.Parcelable import android.os.Parcelable
import android.util.Log import android.util.Log
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64 import com.kunzisoft.encrypt.Signature.fingerprintToUrlSafeBase64
import com.kunzisoft.keepass.model.WebOrigin.Companion.RELYING_PARTY_DEFAULT_PROTOCOL import com.kunzisoft.keepass.model.WebOrigin.Companion.RELYING_PARTY_DEFAULT_PROTOCOL
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize