Merge branch 'feature/passkeys_validation' into feature/Passkeys

This commit is contained in:
J-Jamet
2025-09-02 12:26:30 +02:00
25 changed files with 302 additions and 358 deletions

View File

@@ -38,15 +38,15 @@ android {
}
packaging {
resources.excludes.add("META-INF/versions/9/OSGI-INF/MANIFEST.MF") // bouncycastle need this
// Bouncy castle bug https://github.com/bcgit/bc-java/issues/1685
resources.pickFirsts.add('META-INF/versions/9/OSGI-INF/MANIFEST.MF')
}
}
dependencies {
// Crypto
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.81'
androidTestImplementation 'org.testng:testng:6.9.6'
androidTestImplementation "androidx.test:runner:$android_test_version"
testImplementation "androidx.test:runner:$android_test_version"
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright 202 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.encrypt
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64
import org.junit.Assert
import org.junit.Test
class AppSignatureTest {
@Test
fun testSingleSignature() {
// Generate random input
val fingerprint = "A7:5C:63:72:A0:B6:7D:B0:16:86:B4:7D:F6:8C:91:51:6E:E1:62:29:EE:C4:C0:C6:7D:35:5E:32:20:7C:66:17"
val expected = "p1xjcqC2fbAWhrR99oyRUW7hYinuxMDGfTVeMiB8Zhc"
Assert.assertEquals("Check fingerprint app", expected, fingerprintToUrlSafeBase64(fingerprint))
}
@Test
fun testMultipleSignature() {
// Generate random input
val fingerprint = "A7:5C:63:72:A0:B6:7D:B0:16:86:B4:7D:F6:8C:91:51:6E:E1:62:29:EE:C4:C0:C6:7D:35:5E:32:20:7C:66:17##SIG##DB:25:8A:A6:19:08:9B:D1:3D:BA:71:9E:5A:DA:EC:FF:7F:12:C8:8F:67:AD:68:3C:1F:BC:F2:28:B3:88:BD:91"
val expected = "p1xjcqC2fbAWhrR99oyRUW7hYinuxMDGfTVeMiB8Zhc"
Assert.assertEquals("Check fingerprint app", expected, fingerprintToUrlSafeBase64(fingerprint))
}
}

View File

@@ -1,5 +1,7 @@
package com.kunzisoft.asymmetric
package com.kunzisoft.encrypt
import com.kunzisoft.encrypt.HashManager.fingerprintToUrlSafeBase64
import org.junit.Assert
import org.junit.Test
class SignatureTest {
@@ -154,4 +156,22 @@ class SignatureTest {
}
// endregion
@Test
fun testSingleSignature() {
// Generate random input
val fingerprint = "A7:5C:63:72:A0:B6:7D:B0:16:86:B4:7D:F6:8C:91:51:6E:E1:62:29:EE:C4:C0:C6:7D:35:5E:32:20:7C:66:17"
val expected = "p1xjcqC2fbAWhrR99oyRUW7hYinuxMDGfTVeMiB8Zhc"
Assert.assertEquals("Check fingerprint app", expected, fingerprintToUrlSafeBase64(fingerprint))
}
@Test
fun testMultipleSignature() {
// Generate random input
val fingerprint = "A7:5C:63:72:A0:B6:7D:B0:16:86:B4:7D:F6:8C:91:51:6E:E1:62:29:EE:C4:C0:C6:7D:35:5E:32:20:7C:66:17##SIG##DB:25:8A:A6:19:08:9B:D1:3D:BA:71:9E:5A:DA:EC:FF:7F:12:C8:8F:67:AD:68:3C:1F:BC:F2:28:B3:88:BD:91"
val expected = "p1xjcqC2fbAWhrR99oyRUW7hYinuxMDGfTVeMiB8Zhc"
Assert.assertEquals("Check fingerprint app", expected, fingerprintToUrlSafeBase64(fingerprint))
}
}

View File

@@ -1,230 +0,0 @@
package com.kunzisoft.asymmetric
import android.util.Log
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
import org.bouncycastle.crypto.util.OpenSSHPrivateKeyUtil
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator
import org.bouncycastle.util.BigIntegers
import org.bouncycastle.util.io.pem.PemWriter
import java.io.StringReader
import java.io.StringWriter
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.security.Security
import java.security.Signature
import java.security.spec.ECGenParameterSpec
object Signature {
// 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
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
}
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 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 writer = StringWriter()
val pemWriter = PemWriter(writer)
pemWriter.writeObject(pemObjectGenerator)
pemWriter.close()
val privateKeyInPem = writer.toString().trim()
writer.close()
return privateKeyInPem
}
fun generateKeyPair(keyTypeIdList: List<Long>): Pair<KeyPair, Long>? {
for (typeId in keyTypeIdList) {
if (typeId == ES256_ALGORITHM) {
val es256CurveNameBC = "secp256r1"
val spec = ECGenParameterSpec(es256CurveNameBC)
val keyPairGen =
KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME)
keyPairGen.initialize(spec)
val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, ES256_ALGORITHM)
} else if (typeId == 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) {
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) {
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>? {
// 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<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 publicKeyMap
}
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")
return null
}
}

View File

@@ -0,0 +1,237 @@
package com.kunzisoft.encrypt
import android.util.Log
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPublicKey
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openssl.PEMParser
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter
import org.bouncycastle.openssl.jcajce.JcaPKCS8Generator
import org.bouncycastle.util.BigIntegers
import org.bouncycastle.util.io.pem.PemWriter
import java.io.StringReader
import java.io.StringWriter
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
import java.security.Security
import java.security.Signature
import java.security.spec.ECGenParameterSpec
class 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
const val ED_DSA_ALGORITHM: Long = -8
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
}
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 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 writer = StringWriter()
val pemWriter = PemWriter(writer)
pemWriter.writeObject(pemObjectGenerator)
pemWriter.close()
val privateKeyInPem = writer.toString().trim()
writer.close()
return privateKeyInPem
}
fun generateKeyPair(keyTypeIdList: List<Long>): Pair<KeyPair, Long>? {
for (typeId in keyTypeIdList) {
if (typeId == ES256_ALGORITHM) {
val es256CurveNameBC = "secp256r1"
val spec = ECGenParameterSpec(es256CurveNameBC)
val keyPairGen =
KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME)
keyPairGen.initialize(spec)
val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, ES256_ALGORITHM)
} else if (typeId == 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) {
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) {
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>? {
// 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<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 publicKeyMap
}
Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")
return null
}
}
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2025 Jeremy Jamet / Kunzisoft.
*
* This file is part of KeePassDX.
*
* KeePassDX is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KeePassDX is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with KeePassDX. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kunzisoft.random
import java.security.SecureRandom
class KeePassDXRandom {
companion object {
private val internalSecureRandom: SecureRandom = SecureRandom()
fun generateCredentialId(): ByteArray {
// see https://w3c.github.io/webauthn/#credential-id
val size = 16
val credentialId = ByteArray(size)
internalSecureRandom.nextBytes(credentialId)
return credentialId
}
}
}