diff --git a/crypto/src/androidTest/java/com/kunzisoft/asymmetric/SignatureTest.kt b/crypto/src/androidTest/java/com/kunzisoft/asymmetric/SignatureTest.kt index 34867e4d9..c6e3a3d17 100644 --- a/crypto/src/androidTest/java/com/kunzisoft/asymmetric/SignatureTest.kt +++ b/crypto/src/androidTest/java/com/kunzisoft/asymmetric/SignatureTest.kt @@ -1,12 +1,19 @@ 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 java.io.File +import java.io.FileWriter +import kotlin.io.path.Path class SignatureTest { // All private keys are for testing only. // DO NOT USE THEM + + // region ES256 private val es256PemInKeePassXC = """ -----BEGIN PRIVATE KEY----- @@ -48,10 +55,14 @@ class SignatureTest { assert(keyTypeId == Signature.ES256_ALGORITHM) 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 = """ -----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCaunVJEhLHl7/f @@ -99,4 +110,58 @@ class SignatureTest { assert(keyTypeId == Signature.RS256_ALGORITHM) 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 + } \ No newline at end of file diff --git a/crypto/src/main/java/com/kunzisoft/asymmetric/Signature.kt b/crypto/src/main/java/com/kunzisoft/asymmetric/Signature.kt index f1aaee828..0c8200d3a 100644 --- a/crypto/src/main/java/com/kunzisoft/asymmetric/Signature.kt +++ b/crypto/src/main/java/com/kunzisoft/asymmetric/Signature.kt @@ -2,7 +2,11 @@ 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 @@ -27,6 +31,8 @@ object Signature { 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()) @@ -39,10 +45,11 @@ object Signature { "EC" -> "SHA256withECDSA" "ECDSA" -> "SHA256withECDSA" "RSA" -> "SHA256withRSA" + "Ed25519" -> "Ed25519" else -> 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 } val sig = Signature.getInstance(algorithmSignature, BouncyCastleProvider.PROVIDER_NAME) @@ -62,6 +69,13 @@ object Signature { } 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) @@ -86,13 +100,19 @@ object Signature { keyPairGen.initialize(spec) val keyPair = keyPairGen.genKeyPair() return Pair(keyPair, ES256_ALGORITHM) - } else if (typeId == RS256_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) } } @@ -108,12 +128,19 @@ object Signature { } } 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? { + + // 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( @@ -128,8 +155,9 @@ object Signature { val es256KeyTypeId = 2 val es256EllipticCurveP256Id = 1 - publicKeyMap[1] = es256KeyTypeId - publicKeyMap[3] = ES256_ALGORITHM + publicKeyMap[keyTypeLabel] = es256KeyTypeId + publicKeyMap[algorithmLabel] = ES256_ALGORITHM + publicKeyMap[-1] = es256EllipticCurveP256Id val ecPoint = publicKeyIn.q @@ -154,8 +182,8 @@ object Signature { val rs256ExponentSizeInBytes = 3 val publicKeyMap = mutableMapOf() - publicKeyMap[1] = rs256KeyTypeId - publicKeyMap[3] = RS256_ALGORITHM + publicKeyMap[keyTypeLabel] = rs256KeyTypeId + publicKeyMap[algorithmLabel] = RS256_ALGORITHM publicKeyMap[-1] = BigIntegers.asUnsignedByteArray(rs256KeySizeInBytes, publicKeyIn.modulus) publicKeyMap[-2] = @@ -164,6 +192,36 @@ object Signature { 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")