Add support for ed25519

This commit is contained in:
cali-95
2025-08-24 18:34:29 +02:00
committed by J-Jamet
parent f2f4c1e63d
commit 736cafbcc2
2 changed files with 130 additions and 7 deletions

View File

@@ -1,12 +1,19 @@
package com.kunzisoft.asymmetric package com.kunzisoft.asymmetric
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCEdDSAPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.edec.BCXDHPrivateKey
import org.junit.Test import org.junit.Test
import java.io.File
import java.io.FileWriter
import kotlin.io.path.Path
class SignatureTest { class SignatureTest {
// All private keys are for testing only. // All private keys are for testing only.
// DO NOT USE THEM // DO NOT USE THEM
// region ES256
private val es256PemInKeePassXC = private val es256PemInKeePassXC =
""" """
-----BEGIN PRIVATE KEY----- -----BEGIN PRIVATE KEY-----
@@ -48,10 +55,14 @@ class SignatureTest {
assert(keyTypeId == Signature.ES256_ALGORITHM) assert(keyTypeId == Signature.ES256_ALGORITHM)
assert(privateKeyPem.contains("-----BEGIN PRIVATE KEY-----", true)) assert(privateKeyPem.contains("-----BEGIN PRIVATE KEY-----", true))
assert(privateKeyPem.contains("-----BEGIN EC PRIVATE KEY-----", true).not()) assert( privateKeyPem.contains("-----BEGIN EC PRIVATE KEY-----", true).not())
} }
// endregion
// region RSA
private val rsa256PemIn = """ private val rsa256PemIn = """
-----BEGIN PRIVATE KEY----- -----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCaunVJEhLHl7/f MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCaunVJEhLHl7/f
@@ -99,4 +110,58 @@ class SignatureTest {
assert(keyTypeId == Signature.RS256_ALGORITHM) assert(keyTypeId == Signature.RS256_ALGORITHM)
assert(privateKeyPem.contains("-----BEGIN PRIVATE KEY-----", true)) assert(privateKeyPem.contains("-----BEGIN PRIVATE KEY-----", true))
} }
// endregion
// region ED25519
private val ed25519PemInShort = """
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEILBoCo4+IXxIuwN36/oaEsPgbe6WYJcV9YW+xnprDF4H
-----END PRIVATE KEY-----
""".trimIndent()
private val ed25519PemInLong = """
-----BEGIN PRIVATE KEY-----
MFECAQEwBQYDK2VwBCIEIESP8edVGbqoR/pKNmy7j7FV8Y68zrIi/5VEuAJ281K6
gSEAyJU1wQNaJUeyxPcWjN7xZKZUhCRoIFS/MQvbdd4QE7Q=
-----END PRIVATE KEY-----
""".trimIndent()
private val ed25519PemOut = """
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIESP8edVGbqoR/pKNmy7j7FV8Y68zrIi/5VEuAJ281K6
-----END PRIVATE KEY-----
""".trimIndent()
@Test
fun testEd25519KeyConverionShortIn() {
val privateKey = Signature.createPrivateKey(ed25519PemInShort)
val pemOut = Signature.convertPrivateKeyToPem(privateKey)
assert(pemOut == ed25519PemInShort)
}
@Test
fun testEd25519KeyConverionLongIn() {
val privateKey = Signature.createPrivateKey(ed25519PemInLong)
val pemOut = Signature.convertPrivateKeyToPem(privateKey)
assert(pemOut == ed25519PemOut)
}
@Test
fun testEd25519KeyGenAndConversion() {
val (keyPair, keyTypeId) = Signature.generateKeyPair(listOf(Signature.ED_DSA_ALGORITHM))!!
val privateKeyPem = Signature.convertPrivateKeyToPem(keyPair.private)
assert(keyTypeId == Signature.ED_DSA_ALGORITHM)
assert(privateKeyPem.contains("-----BEGIN PRIVATE KEY-----", true))
assert(privateKeyPem.contains("-----BEGIN EC PRIVATE KEY-----", true).not())
}
// endregion
} }

View File

@@ -2,7 +2,11 @@ package com.kunzisoft.asymmetric
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.util.OpenSSHPrivateKeyUtil
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey 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.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openssl.PEMParser import org.bouncycastle.openssl.PEMParser
@@ -27,6 +31,8 @@ object Signature {
const val RS256_ALGORITHM: Long = -257 const val RS256_ALGORITHM: Long = -257
private const val RS256_KEY_SIZE_IN_BITS = 2048 private const val RS256_KEY_SIZE_IN_BITS = 2048
const val ED_DSA_ALGORITHM: Long = -8
init { init {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider()) Security.addProvider(BouncyCastleProvider())
@@ -39,10 +45,11 @@ object Signature {
"EC" -> "SHA256withECDSA" "EC" -> "SHA256withECDSA"
"ECDSA" -> "SHA256withECDSA" "ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA" "RSA" -> "SHA256withRSA"
"Ed25519" -> "Ed25519"
else -> null else -> null
} }
if (algorithmSignature == null) { if (algorithmSignature == null) {
Log.e(this::class.java.simpleName, "sign: privateKeyPem has an unknown algorithm") Log.e(this::class.java.simpleName, "sign: the algorithm $algorithmKey is unknown")
return null return null
} }
val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME) val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME)
@@ -62,6 +69,13 @@ object Signature {
} }
fun convertPrivateKeyToPem(privateKey: PrivateKey): String { 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 noOutputEncryption = null
val pemObjectGenerator = JcaPKCS8Generator(privateKey, noOutputEncryption) val pemObjectGenerator = JcaPKCS8Generator(privateKey, noOutputEncryption)
@@ -86,13 +100,19 @@ object Signature {
keyPairGen.initialize(spec) keyPairGen.initialize(spec)
val keyPair = keyPairGen.genKeyPair() val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, ES256_ALGORITHM) return Pair(keyPair, ES256_ALGORITHM)
} else if (typeId == RS256_ALGORITHM) {
} else if (typeId == 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) {
val keyPairGen =
KeyPairGenerator.getInstance("Ed25519", BouncyCastleProvider.PROVIDER_NAME)
val keyPair = keyPairGen.genKeyPair()
return Pair(keyPair, ED_DSA_ALGORITHM)
} }
} }
@@ -108,12 +128,19 @@ object Signature {
} }
} else if (keyTypeId == RS256_ALGORITHM) { } else if (keyTypeId == RS256_ALGORITHM) {
return publicKeyIn.encoded return publicKeyIn.encoded
} else if (keyTypeId == ED_DSA_ALGORITHM) {
return publicKeyIn.encoded
} }
Log.e(this::class.java.simpleName, "convertPublicKey: unknown key type id found") Log.e(this::class.java.simpleName, "convertPublicKey: unknown key type id found")
return null 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
val keyTypeLabel = 1
val algorithmLabel = 3
if (keyTypeId == ES256_ALGORITHM) { if (keyTypeId == ES256_ALGORITHM) {
if (publicKeyIn !is BCECPublicKey) { if (publicKeyIn !is BCECPublicKey) {
Log.e( Log.e(
@@ -128,8 +155,9 @@ object Signature {
val es256KeyTypeId = 2 val es256KeyTypeId = 2
val es256EllipticCurveP256Id = 1 val es256EllipticCurveP256Id = 1
publicKeyMap[1] = es256KeyTypeId publicKeyMap[keyTypeLabel] = es256KeyTypeId
publicKeyMap[3] = ES256_ALGORITHM publicKeyMap[algorithmLabel] = ES256_ALGORITHM
publicKeyMap[-1] = es256EllipticCurveP256Id publicKeyMap[-1] = es256EllipticCurveP256Id
val ecPoint = publicKeyIn.q val ecPoint = publicKeyIn.q
@@ -154,8 +182,8 @@ object Signature {
val rs256ExponentSizeInBytes = 3 val rs256ExponentSizeInBytes = 3
val publicKeyMap = mutableMapOf<Int, Any>() val publicKeyMap = mutableMapOf<Int, Any>()
publicKeyMap[1] = rs256KeyTypeId publicKeyMap[keyTypeLabel] = rs256KeyTypeId
publicKeyMap[3] = RS256_ALGORITHM publicKeyMap[algorithmLabel] = RS256_ALGORITHM
publicKeyMap[-1] = publicKeyMap[-1] =
BigIntegers.asUnsignedByteArray(rs256KeySizeInBytes, publicKeyIn.modulus) BigIntegers.asUnsignedByteArray(rs256KeySizeInBytes, publicKeyIn.modulus)
publicKeyMap[-2] = publicKeyMap[-2] =
@@ -164,6 +192,36 @@ object Signature {
publicKeyIn.publicExponent publicKeyIn.publicExponent
) )
return publicKeyMap 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") Log.e(this::class.java.simpleName, "convertPublicKeyToMap: no known key type id found")