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,86 +20,89 @@ 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
const val ES256_ALGORITHM: Long = -7
const val RS256_ALGORITHM: Long = -257
private const val RS256_KEY_SIZE_IN_BITS = 2048
// see at https://www.iana.org/assignments/cose/cose.xhtml const val ED_DSA_ALGORITHM: Long = -8
const val ES256_ALGORITHM: Long = -7
const val RS256_ALGORITHM: Long = -257
private const val RS256_KEY_SIZE_IN_BITS = 2048
const val ED_DSA_ALGORITHM: Long = -8 init {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
}
init { fun sign(privateKeyPem: String, message: ByteArray): ByteArray? {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) val privateKey = createPrivateKey(privateKeyPem)
Security.addProvider(BouncyCastleProvider()) val algorithmKey = privateKey.algorithm
val algorithmSignature = when (algorithmKey) {
"EC" -> "SHA256withECDSA"
"ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
"Ed25519" -> "Ed25519"
else -> null
} }
if (algorithmSignature == null) {
fun sign(privateKeyPem: String, message: ByteArray): ByteArray? { Log.e(this::class.java.simpleName, "sign: the algorithm $algorithmKey is unknown")
val privateKey = createPrivateKey(privateKeyPem) return null
val algorithmKey = privateKey.algorithm
val algorithmSignature = when (algorithmKey) {
"EC" -> "SHA256withECDSA"
"ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
"Ed25519" -> "Ed25519"
else -> null
}
if (algorithmSignature == null) {
Log.e(this::class.java.simpleName, "sign: the algorithm $algorithmKey is unknown")
return null
}
val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME)
sig.initSign(privateKey)
sig.update(message)
return sig.sign()
} }
val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME)
sig.initSign(privateKey)
sig.update(message)
return sig.sign()
}
fun createPrivateKey(privateKeyPem: String): PrivateKey { fun createPrivateKey(privateKeyPem: String): PrivateKey {
val targetReader = StringReader(privateKeyPem) val targetReader = StringReader(privateKeyPem)
val pemParser = PEMParser(targetReader) val pemParser = PEMParser(targetReader)
val privateKeyInfo = pemParser.readObject() as PrivateKeyInfo val privateKeyInfo = pemParser.readObject() as PrivateKeyInfo
val privateKey = JcaPEMKeyConverter().getPrivateKey(privateKeyInfo) val privateKey = JcaPEMKeyConverter().getPrivateKey(privateKeyInfo)
pemParser.close() pemParser.close()
targetReader.close() targetReader.close()
return privateKey return privateKey
}
fun convertPrivateKeyToPem(privateKey: PrivateKey): String {
var useV1Info = false
if (privateKey is BCEdDSAPrivateKey) {
// to generate PEM, which are compatible to KeepassXC
useV1Info = true
} }
System.setProperty(
"org.bouncycastle.pkcs8.v1_info_only",
useV1Info.toString().lowercase()
)
fun convertPrivateKeyToPem(privateKey: PrivateKey): String { val noOutputEncryption = null
var useV1Info = false val pemObjectGenerator = JcaPKCS8Generator(privateKey, noOutputEncryption)
if (privateKey is BCEdDSAPrivateKey) {
// to generate PEM, which are compatible to KeepassXC
useV1Info = true
}
System.setProperty(
"org.bouncycastle.pkcs8.v1_info_only",
useV1Info.toString().lowercase()
)
val noOutputEncryption = null val writer = StringWriter()
val pemObjectGenerator = JcaPKCS8Generator(privateKey, noOutputEncryption) val pemWriter = PemWriter(writer)
pemWriter.writeObject(pemObjectGenerator)
pemWriter.close()
val writer = StringWriter() val privateKeyInPem = writer.toString().trim()
val pemWriter = PemWriter(writer) writer.close()
pemWriter.writeObject(pemObjectGenerator) return privateKeyInPem
pemWriter.close() }
val privateKeyInPem = writer.toString().trim() fun generateKeyPair(keyTypeIdList: List<Long>): Pair<KeyPair, Long>? {
writer.close()
return privateKeyInPem
}
fun generateKeyPair(keyTypeIdList: List<Long>): Pair<KeyPair, Long>? { for (typeId in keyTypeIdList) {
when (typeId) {
for (typeId in keyTypeIdList) { ES256_ALGORITHM -> {
if (typeId == ES256_ALGORITHM) {
val es256CurveNameBC = "secp256r1" val es256CurveNameBC = "secp256r1"
val spec = ECGenParameterSpec(es256CurveNameBC) val spec = ECGenParameterSpec(es256CurveNameBC)
val keyPairGen = val keyPairGen =
@@ -105,133 +111,247 @@ 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")
return null
} }
fun convertPublicKey(publicKeyIn: PublicKey, keyTypeId: Long): ByteArray? { Log.e(this::class.java.simpleName, "generateKeyPair: no known key type id found")
if (keyTypeId == ES256_ALGORITHM) { return null
if (publicKeyIn is BCECPublicKey) { }
publicKeyIn.setPointFormat("UNCOMPRESSED")
return publicKeyIn.encoded fun convertPublicKey(publicKeyIn: PublicKey, keyTypeId: Long): ByteArray? {
} if (keyTypeId == ES256_ALGORITHM) {
} else if (keyTypeId == RS256_ALGORITHM) { if (publicKeyIn is BCECPublicKey) {
return publicKeyIn.encoded publicKeyIn.setPointFormat("UNCOMPRESSED")
} else if (keyTypeId == ED_DSA_ALGORITHM) {
return publicKeyIn.encoded return publicKeyIn.encoded
} }
Log.e(this::class.java.simpleName, "convertPublicKey: unknown key type id found") } else if (keyTypeId == RS256_ALGORITHM) {
return null return publicKeyIn.encoded
} else if (keyTypeId == ED_DSA_ALGORITHM) {
return publicKeyIn.encoded
} }
Log.e(this::class.java.simpleName, "convertPublicKey: unknown key type id found")
return null
}
fun convertPublicKeyToMap(publicKeyIn: PublicKey, keyTypeId: Long): Map<Int, Any>? { fun convertPublicKeyToMap(publicKeyIn: PublicKey, keyTypeId: Long): Map<Int, Any>? {
// https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters // https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters
val keyTypeLabel = 1 val keyTypeLabel = 1
val algorithmLabel = 3 val algorithmLabel = 3
if (keyTypeId == ES256_ALGORITHM) { if (keyTypeId == ES256_ALGORITHM) {
if (publicKeyIn !is BCECPublicKey) { if (publicKeyIn !is BCECPublicKey) {
Log.e( Log.e(
this::class.java.simpleName, this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $ES256_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}" "publicKey object has wrong type for keyTypeId $ES256_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
}
// constants see at https://w3c.github.io/webauthn/#example-bdbd14cc
val publicKeyMap = mutableMapOf<Int, Any>()
val es256KeyTypeId = 2
val es256EllipticCurveP256Id = 1
publicKeyMap[keyTypeLabel] = es256KeyTypeId
publicKeyMap[algorithmLabel] = ES256_ALGORITHM
publicKeyMap[-1] = es256EllipticCurveP256Id
val ecPoint = publicKeyIn.q
publicKeyMap[-2] = ecPoint.xCoord.encoded
publicKeyMap[-3] = ecPoint.yCoord.encoded
return publicKeyMap
} else if (keyTypeId == RS256_ALGORITHM) {
if (publicKeyIn !is BCRSAPublicKey) {
Log.e(
this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $RS256_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
}
// constants see at https://w3c.github.io/webauthn/#example-8dfabc00
val rs256KeySizeInBytes = RS256_KEY_SIZE_IN_BITS / 8
val rs256KeyTypeId = 3
val rs256ExponentSizeInBytes = 3
val publicKeyMap = mutableMapOf<Int, Any>()
publicKeyMap[keyTypeLabel] = rs256KeyTypeId
publicKeyMap[algorithmLabel] = RS256_ALGORITHM
publicKeyMap[-1] =
BigIntegers.asUnsignedByteArray(rs256KeySizeInBytes, publicKeyIn.modulus)
publicKeyMap[-2] =
BigIntegers.asUnsignedByteArray(
rs256ExponentSizeInBytes,
publicKeyIn.publicExponent
)
return publicKeyMap
} else if (keyTypeId == ED_DSA_ALGORITHM) {
if (publicKeyIn !is BCEdDSAPublicKey) {
Log.e(
this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $ED_DSA_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
}
val publicKeyMap = mutableMapOf<Int, Any>()
// https://www.rfc-editor.org/rfc/rfc9053#name-key-object-parameters
val octetKeyPairId = 1
val curveLabel = -1
val ed25519CurveId = 6
val publicKeyLabel = -2
publicKeyMap[keyTypeLabel] = octetKeyPairId
publicKeyMap[algorithmLabel] = ED_DSA_ALGORITHM
publicKeyMap[curveLabel] = ed25519CurveId
val length = Ed25519PublicKeyParameters.KEY_SIZE
publicKeyMap[publicKeyLabel] = BigIntegers.asUnsignedByteArray(
length,
BigIntegers.fromUnsignedByteArray(publicKeyIn.pointEncoding)
) )
return null
}
// constants see at https://w3c.github.io/webauthn/#example-bdbd14cc
val publicKeyMap = mutableMapOf<Int, Any>()
return publicKeyMap val es256KeyTypeId = 2
val es256EllipticCurveP256Id = 1
publicKeyMap[keyTypeLabel] = es256KeyTypeId
publicKeyMap[algorithmLabel] = ES256_ALGORITHM
publicKeyMap[-1] = es256EllipticCurveP256Id
val ecPoint = publicKeyIn.q
publicKeyMap[-2] = ecPoint.xCoord.encoded
publicKeyMap[-3] = ecPoint.yCoord.encoded
return publicKeyMap
} else if (keyTypeId == RS256_ALGORITHM) {
if (publicKeyIn !is BCRSAPublicKey) {
Log.e(
this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $RS256_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
} }
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found") // constants see at https://w3c.github.io/webauthn/#example-8dfabc00
val rs256KeySizeInBytes = RS256_KEY_SIZE_IN_BITS / 8
val rs256KeyTypeId = 3
val rs256ExponentSizeInBytes = 3
val publicKeyMap = mutableMapOf<Int, Any>()
publicKeyMap[keyTypeLabel] = rs256KeyTypeId
publicKeyMap[algorithmLabel] = RS256_ALGORITHM
publicKeyMap[-1] =
BigIntegers.asUnsignedByteArray(rs256KeySizeInBytes, publicKeyIn.modulus)
publicKeyMap[-2] =
BigIntegers.asUnsignedByteArray(
rs256ExponentSizeInBytes,
publicKeyIn.publicExponent
)
return publicKeyMap
} else if (keyTypeId == ED_DSA_ALGORITHM) {
if (publicKeyIn !is BCEdDSAPublicKey) {
Log.e(
this::class.java.simpleName,
"publicKey object has wrong type for keyTypeId $ED_DSA_ALGORITHM: ${publicKeyIn.javaClass.canonicalName}"
)
return null
}
val publicKeyMap = mutableMapOf<Int, Any>()
// https://www.rfc-editor.org/rfc/rfc9053#name-key-object-parameters
val octetKeyPairId = 1
val curveLabel = -1
val ed25519CurveId = 6
val publicKeyLabel = -2
publicKeyMap[keyTypeLabel] = octetKeyPairId
publicKeyMap[algorithmLabel] = ED_DSA_ALGORITHM
publicKeyMap[curveLabel] = ed25519CurveId
val length = Ed25519PublicKeyParameters.KEY_SIZE
publicKeyMap[publicKeyLabel] = BigIntegers.asUnsignedByteArray(
length,
BigIntegers.fromUnsignedByteArray(publicKeyIn.pointEncoding)
)
return publicKeyMap
}
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")
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 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