mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
fix: Small refactoring
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user