diff --git a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt index 30503465b..b5a360f71 100644 --- a/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt +++ b/app/src/main/java/com/kunzisoft/keepass/credentialprovider/passkey/util/PasskeyHelper.kt @@ -40,8 +40,8 @@ import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.ProviderCreateCredentialRequest import androidx.credentials.provider.ProviderGetCredentialRequest import com.kunzisoft.encrypt.Base64Helper.Companion.b64Encode -import com.kunzisoft.encrypt.HashManager.getApplicationFingerprints 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.AuthenticatorAttestationResponse 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.Passkey import com.kunzisoft.keepass.model.SearchInfo -import com.kunzisoft.keepass.model.WebOrigin import com.kunzisoft.keepass.utils.StringUtil.toHexString import com.kunzisoft.keepass.utils.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers @@ -271,7 +270,7 @@ object PasskeyHelper { keyStore.load(null) val hmacKey = try { keyStore.getKey(NAME_OF_HMAC_KEY, null) as SecretKey - } catch (e: Exception) { + } catch (_: Exception) { // key not found generateKey() } @@ -338,7 +337,6 @@ object PasskeyHelper { providedClientDataHash: ByteArray?, callingAppInfo: CallingAppInfo?, assets: AssetManager, - relyingParty: String, onOriginRetrieved: (appOrigin: AppOrigin, clientDataHash: ByteArray) -> Unit, onOriginNotRetrieved: (appOrigin: AppOrigin, androidOriginString: String) -> Unit ) { @@ -356,9 +354,6 @@ object PasskeyHelper { packageName = callingAppInfo.packageName, fingerprint = callingAppInfo.signingInfo.getApplicationFingerprints() ) - val webOrigin = WebOrigin.fromRelyingParty( - relyingParty = relyingParty - ) // Check if the webDomain is validated for the withContext(Dispatchers.Main) { if (callOrigin != null && providedClientDataHash != null) { @@ -436,7 +431,6 @@ object PasskeyHelper { providedClientDataHash = clientDataHash, callingAppInfo = callingAppInfo, assets = assetManager, - relyingParty = relyingParty, onOriginRetrieved = { appInfoToStore, clientDataHash -> passkeyCreated.invoke( passkey, @@ -527,7 +521,6 @@ object PasskeyHelper { providedClientDataHash = clientDataHash, callingAppInfo = callingAppInfo, assets = assetManager, - relyingParty = requestOptions.rpId, onOriginRetrieved = { appOrigin, clientDataHash -> result.invoke( PublicKeyCredentialUsageParameters( diff --git a/crypto/src/androidTest/java/com/kunzisoft/encrypt/AESTest.kt b/crypto/src/androidTest/java/com/kunzisoft/encrypt/AESTest.kt index fa285c0db..993cea174 100644 --- a/crypto/src/androidTest/java/com/kunzisoft/encrypt/AESTest.kt +++ b/crypto/src/androidTest/java/com/kunzisoft/encrypt/AESTest.kt @@ -24,7 +24,7 @@ import org.junit.Assert.assertArrayEquals import org.junit.Test import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream -import java.util.* +import java.util.Random import javax.crypto.Cipher import javax.crypto.CipherInputStream import javax.crypto.CipherOutputStream diff --git a/crypto/src/androidTest/java/com/kunzisoft/encrypt/SignatureTest.kt b/crypto/src/androidTest/java/com/kunzisoft/encrypt/SignatureTest.kt index bdc13cebd..53c18eba7 100644 --- a/crypto/src/androidTest/java/com/kunzisoft/encrypt/SignatureTest.kt +++ b/crypto/src/androidTest/java/com/kunzisoft/encrypt/SignatureTest.kt @@ -1,6 +1,6 @@ package com.kunzisoft.encrypt -import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64 +import com.kunzisoft.encrypt.Signature.fingerprintToUrlSafeBase64 import org.junit.Assert import org.junit.Test diff --git a/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt b/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt index 398273c1f..1c7e1e573 100644 --- a/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt +++ b/crypto/src/main/java/com/kunzisoft/encrypt/HashManager.kt @@ -19,11 +19,6 @@ */ 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 @@ -31,14 +26,9 @@ 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 { @@ -117,115 +107,4 @@ object HashManager { 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? { - 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 ##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) - } } diff --git a/crypto/src/main/java/com/kunzisoft/encrypt/Signature.kt b/crypto/src/main/java/com/kunzisoft/encrypt/Signature.kt index 365ac2930..73432335e 100644 --- a/crypto/src/main/java/com/kunzisoft/encrypt/Signature.kt +++ b/crypto/src/main/java/com/kunzisoft/encrypt/Signature.kt @@ -1,5 +1,8 @@ package com.kunzisoft.encrypt +import android.content.pm.SigningInfo +import android.os.Build +import android.util.AndroidException import android.util.Log import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters @@ -17,86 +20,89 @@ import java.io.StringReader import java.io.StringWriter import java.security.KeyPair import java.security.KeyPairGenerator +import java.security.MessageDigest import java.security.PrivateKey import java.security.PublicKey import java.security.Security import java.security.Signature +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate 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 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 - const val ED_DSA_ALGORITHM: Long = -8 + init { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.addProvider(BouncyCastleProvider()) + } - init { - Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) - Security.addProvider(BouncyCastleProvider()) + fun sign(privateKeyPem: String, message: ByteArray): ByteArray? { + val privateKey = createPrivateKey(privateKeyPem) + val algorithmKey = privateKey.algorithm + val algorithmSignature = when (algorithmKey) { + "EC" -> "SHA256withECDSA" + "ECDSA" -> "SHA256withECDSA" + "RSA" -> "SHA256withRSA" + "Ed25519" -> "Ed25519" + else -> null } - - fun sign(privateKeyPem: String, message: ByteArray): ByteArray? { - val privateKey = createPrivateKey(privateKeyPem) - 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() + 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() + } - fun createPrivateKey(privateKeyPem: String): PrivateKey { - val targetReader = StringReader(privateKeyPem) - val pemParser = PEMParser(targetReader) - val privateKeyInfo = pemParser.readObject() as PrivateKeyInfo - val privateKey = JcaPEMKeyConverter().getPrivateKey(privateKeyInfo) - pemParser.close() - targetReader.close() - return privateKey + fun createPrivateKey(privateKeyPem: String): PrivateKey { + val targetReader = StringReader(privateKeyPem) + val pemParser = PEMParser(targetReader) + val privateKeyInfo = pemParser.readObject() as PrivateKeyInfo + val privateKey = JcaPEMKeyConverter().getPrivateKey(privateKeyInfo) + pemParser.close() + targetReader.close() + 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 { - 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() - ) + val noOutputEncryption = null + val pemObjectGenerator = JcaPKCS8Generator(privateKey, noOutputEncryption) - val noOutputEncryption = null - val pemObjectGenerator = JcaPKCS8Generator(privateKey, noOutputEncryption) + val writer = StringWriter() + val pemWriter = PemWriter(writer) + pemWriter.writeObject(pemObjectGenerator) + pemWriter.close() - val writer = StringWriter() - val pemWriter = PemWriter(writer) - pemWriter.writeObject(pemObjectGenerator) - pemWriter.close() + val privateKeyInPem = writer.toString().trim() + writer.close() + return privateKeyInPem + } - val privateKeyInPem = writer.toString().trim() - writer.close() - return privateKeyInPem - } + fun generateKeyPair(keyTypeIdList: List): Pair? { - fun generateKeyPair(keyTypeIdList: List): Pair? { - - for (typeId in keyTypeIdList) { - if (typeId == ES256_ALGORITHM) { + for (typeId in keyTypeIdList) { + when (typeId) { + ES256_ALGORITHM -> { val es256CurveNameBC = "secp256r1" val spec = ECGenParameterSpec(es256CurveNameBC) val keyPairGen = @@ -105,133 +111,247 @@ class Signature { val keyPair = keyPairGen.genKeyPair() return Pair(keyPair, ES256_ALGORITHM) - } else if (typeId == RS256_ALGORITHM) { + } + RS256_ALGORITHM -> { val keyPairGen = KeyPairGenerator.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME) keyPairGen.initialize(RS256_KEY_SIZE_IN_BITS) val keyPair = keyPairGen.genKeyPair() return Pair(keyPair, RS256_ALGORITHM) - } else if (typeId == ED_DSA_ALGORITHM) { + } + ED_DSA_ALGORITHM -> { val keyPairGen = KeyPairGenerator.getInstance("Ed25519", BouncyCastleProvider.PROVIDER_NAME) val keyPair = keyPairGen.genKeyPair() 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? { - if (keyTypeId == ES256_ALGORITHM) { - if (publicKeyIn is BCECPublicKey) { - publicKeyIn.setPointFormat("UNCOMPRESSED") - return publicKeyIn.encoded - } - } else if (keyTypeId == RS256_ALGORITHM) { - return publicKeyIn.encoded - } else if (keyTypeId == 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? { + if (keyTypeId == ES256_ALGORITHM) { + if (publicKeyIn is BCECPublicKey) { + publicKeyIn.setPointFormat("UNCOMPRESSED") return publicKeyIn.encoded } - Log.e(this::class.java.simpleName, "convertPublicKey: unknown key type id found") - return null + } else if (keyTypeId == RS256_ALGORITHM) { + 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? { + fun convertPublicKeyToMap(publicKeyIn: PublicKey, keyTypeId: Long): Map? { - // https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters - val keyTypeLabel = 1 - val algorithmLabel = 3 + // https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters + val keyTypeLabel = 1 + val algorithmLabel = 3 - if (keyTypeId == ES256_ALGORITHM) { - if (publicKeyIn !is BCECPublicKey) { - Log.e( - this::class.java.simpleName, - "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() - - 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() - 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() - - // 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) + if (keyTypeId == ES256_ALGORITHM) { + if (publicKeyIn !is BCECPublicKey) { + Log.e( + this::class.java.simpleName, + "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() - 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() + 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() + + // 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? { + 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(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) + } } \ No newline at end of file diff --git a/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt b/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt index a66b0a9dd..d4d75f501 100644 --- a/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt +++ b/database/src/main/java/com/kunzisoft/keepass/model/AppOrigin.kt @@ -21,7 +21,7 @@ package com.kunzisoft.keepass.model import android.os.Parcelable 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 kotlinx.parcelize.Parcelize