diff --git a/README.md b/README.md index ac14f4d8..da1c383b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ cryptography-kotlin provides multiplatform API which consists of multiple compon * [Secure random][Secure random] with [kotlin.Random][kotlin.Random] like API which can be used independently of other modules * common API to use different cryptography operations, like [ciphers][ciphers], [digests][digests], [signatures][signatures], [key derivation][key derivation], [Key agreement][Key agreement] -* multiple algorithms definitions, like [AES][AES], [RSA][RSA], [ECDSA][ECDSA], [ECDH][ECDH], [SHA][SHA256], [HMAC][HMAC] +* multiple algorithms definitions, like [AES][AES], [RSA][RSA], [ECDSA][ECDSA], [ECDH][ECDH], [DH][DH], [SHA][SHA256], [HMAC][HMAC] and [PBKDF2][PBKDF2] * multiple cryptography [providers][providers], like [OpenSSL][OpenSSL], [WebCrypto][WebCrypto], [CryptoKit][CryptoKit] and [JDK][JDK] @@ -75,6 +75,8 @@ Additionally, it's possible to use [BOM][BOM] or [Gradle version catalog][Gradle [ECDH]: https://whyoleg.github.io/cryptography-kotlin/api/cryptography-core/dev.whyoleg.cryptography.algorithms/-e-c-d-h/index.html +[DH]: https://whyoleg.github.io/cryptography-kotlin/api/cryptography-core/dev.whyoleg.cryptography.algorithms/-d-h/index.html + [PBKDF2]: https://whyoleg.github.io/cryptography-kotlin/api/cryptography-core/dev.whyoleg.cryptography.algorithms/-p-b-k-d-f2/index.html [HKDF]: https://whyoleg.github.io/cryptography-kotlin/api/cryptography-core/dev.whyoleg.cryptography.algorithms/-h-k-d-f/index.html diff --git a/cryptography-core/src/commonMain/kotlin/algorithms/DH.kt b/cryptography-core/src/commonMain/kotlin/algorithms/DH.kt new file mode 100644 index 00000000..12858104 --- /dev/null +++ b/cryptography-core/src/commonMain/kotlin/algorithms/DH.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.algorithms + +import dev.whyoleg.cryptography.* +import dev.whyoleg.cryptography.materials.key.* +import dev.whyoleg.cryptography.operations.* +import kotlin.jvm.* + +@SubclassOptInRequired(CryptographyProviderApi::class) +public interface DH : CryptographyAlgorithm { + override val id: CryptographyAlgorithmId get() = Companion + + public companion object : CryptographyAlgorithmId("DH") + + public fun publicKeyDecoder(parameters: Parameters): KeyDecoder + public fun privateKeyDecoder(parameters: Parameters): KeyDecoder + public fun keyPairGenerator(parameters: Parameters): KeyGenerator + + public fun parametersDecoder(): KeyDecoder + public fun parametersGenerator(keySize: Int = 2048): KeyGenerator + + @SubclassOptInRequired(CryptographyProviderApi::class) + public interface Parameters : EncodableKey { + public sealed class Format : KeyFormat { + final override fun toString(): String = name + + // DER = Distinguished Encoding Rules + public data object DER : Format() { + override val name: String get() = "DER" + } + + // PEM = Privacy-Enhanced Mail + public data object PEM : Format() { + override val name: String get() = "PEM" + } + } + } + + @SubclassOptInRequired(CryptographyProviderApi::class) + public interface KeyPair : Key { + public val publicKey: PublicKey + public val privateKey: PrivateKey + } + + @SubclassOptInRequired(CryptographyProviderApi::class) + public interface PublicKey : EncodableKey { + public fun sharedSecretGenerator(): SharedSecretGenerator + + public sealed class Format : KeyFormat { + final override fun toString(): String = name + + // DER = Distinguished Encoding Rules (SPKI = SubjectPublicKeyInfo) + public data object DER : Format() { + override val name: String get() = "DER" + } + + // PEM = Privacy-Enhanced Mail (SPKI = SubjectPublicKeyInfo) + public data object PEM : Format() { + override val name: String get() = "PEM" + } + } + } + + @SubclassOptInRequired(CryptographyProviderApi::class) + public interface PrivateKey : EncodableKey { + public fun sharedSecretGenerator(): SharedSecretGenerator + + public sealed class Format : KeyFormat { + final override fun toString(): String = name + + // DER = Distinguished Encoding Rules (via PrivateKeyInfo from PKCS8) + public data object DER : Format() { + override val name: String get() = "DER" + } + + // PEM = Privacy-Enhanced Mail (via PrivateKeyInfo from PKCS8) + public data object PEM : Format() { + override val name: String get() = "PEM" + } + } + } +} \ No newline at end of file diff --git a/cryptography-providers/jdk/src/jvmMain/kotlin/JdkCryptographyProvider.kt b/cryptography-providers/jdk/src/jvmMain/kotlin/JdkCryptographyProvider.kt index 5d978df3..0d80dbdb 100644 --- a/cryptography-providers/jdk/src/jvmMain/kotlin/JdkCryptographyProvider.kt +++ b/cryptography-providers/jdk/src/jvmMain/kotlin/JdkCryptographyProvider.kt @@ -132,6 +132,7 @@ internal class JdkCryptographyProvider(provider: Provider?) : CryptographyProvid RSA.RAW -> JdkRsaRaw(state) ECDSA -> JdkEcdsa(state) ECDH -> JdkEcdh(state) + DH -> JdkDh(state) PBKDF2 -> JdkPbkdf2(state) HKDF -> JdkHkdf(state, this) else -> null diff --git a/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkDh.kt b/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkDh.kt new file mode 100644 index 00000000..9cb8b693 --- /dev/null +++ b/cryptography-providers/jdk/src/jvmMain/kotlin/algorithms/JdkDh.kt @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.jdk.algorithms + +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.materials.key.* +import dev.whyoleg.cryptography.operations.* +import dev.whyoleg.cryptography.providers.base.materials.* +import dev.whyoleg.cryptography.providers.jdk.* +import dev.whyoleg.cryptography.providers.jdk.materials.* +import dev.whyoleg.cryptography.providers.jdk.operations.* +import dev.whyoleg.cryptography.serialization.pem.* +import java.security.spec.* +import javax.crypto.interfaces.* +import javax.crypto.spec.* + +internal class JdkDh(private val state: JdkCryptographyState) : DH { + override fun publicKeyDecoder(parameters: DH.Parameters): KeyDecoder { + return DhPublicKeyDecoder(parameters) + } + + override fun privateKeyDecoder(parameters: DH.Parameters): KeyDecoder { + return DhPrivateKeyDecoder(parameters) + } + + override fun keyPairGenerator(parameters: DH.Parameters): KeyGenerator { + return DhKeyPairGenerator(parameters) + } + + override fun parametersDecoder(): KeyDecoder { + return DhParametersDecoder() + } + + override fun parametersGenerator(keySize: Int): KeyGenerator { + return DhParametersGenerator(keySize) + } + + private inner class DhParametersGenerator( + private val keySize: Int, + ) : JdkKeyPairGenerator(state, "DH") { + override fun JKeyPairGenerator.init() { + initialize(keySize, state.secureRandom) + } + + override fun JKeyPair.convert(): DH.Parameters { + val publicKey = public as DHPublicKey + return DhParameters(state, publicKey.params) + } + } + + private inner class DhParametersDecoder : KeyDecoder { + override suspend fun decodeFromByteArray(format: DH.Parameters.Format, bytes: ByteArray): DH.Parameters { + return decodeFromByteArrayBlocking(format, bytes) + } + + override fun decodeFromByteArrayBlocking(format: DH.Parameters.Format, bytes: ByteArray): DH.Parameters = when (format) { + DH.Parameters.Format.DER -> decodeFromDer(bytes) + DH.Parameters.Format.PEM -> decodeFromDer(unwrapPem(PemLabel.DHParams, bytes)) + } + + private fun decodeFromDer(bytes: ByteArray): DH.Parameters { + val algorithmParameters = state.algorithmParameters("DH") + algorithmParameters.init(bytes) + val parameterSpec = algorithmParameters.getParameterSpec(DHParameterSpec::class.java) + return DhParameters(state, parameterSpec) + } + } + + private inner class DhKeyPairGenerator( + private val parameters: DH.Parameters, + ) : JdkKeyPairGenerator(state, "DH") { + override fun JKeyPairGenerator.init() { + val dhParameters = (parameters as DhParameters).parameterSpec + initialize(dhParameters, state.secureRandom) + } + + override fun JKeyPair.convert(): DH.KeyPair { + return DhKeyPair( + publicKey = DhPublicKey(state, public as DHPublicKey), + privateKey = DhPrivateKey(state, private as DHPrivateKey) + ) + } + } + + private inner class DhPublicKeyDecoder( + private val parameters: DH.Parameters, + ) : JdkPublicKeyDecoder(state, "DH") { + override fun JPublicKey.convert(): DH.PublicKey { + check(this is DHPublicKey) + val dhParameters = (parameters as DhParameters).parameterSpec + check(this.params.p == dhParameters.p && this.params.g == dhParameters.g) { + "Key parameters do not match expected parameters" + } + return DhPublicKey(state, this) + } + + override fun decodeFromByteArrayBlocking(format: DH.PublicKey.Format, bytes: ByteArray): DH.PublicKey = when (format) { + DH.PublicKey.Format.DER -> decodeFromDer(bytes) + DH.PublicKey.Format.PEM -> decodeFromDer(unwrapPem(PemLabel.PublicKey, bytes)) + } + } + + private inner class DhPrivateKeyDecoder( + private val parameters: DH.Parameters, + ) : JdkPrivateKeyDecoder(state, "DH") { + override fun JPrivateKey.convert(): DH.PrivateKey { + check(this is DHPrivateKey) + val dhParameters = (parameters as DhParameters).parameterSpec + check(this.params.p == dhParameters.p && this.params.g == dhParameters.g) { + "Key parameters do not match expected parameters" + } + return DhPrivateKey(state, this) + } + + override fun decodeFromByteArrayBlocking(format: DH.PrivateKey.Format, bytes: ByteArray): DH.PrivateKey = when (format) { + DH.PrivateKey.Format.DER -> decodeFromDer(bytes) + DH.PrivateKey.Format.PEM -> decodeFromDer(unwrapPem(PemLabel.PrivateKey, bytes)) + } + } + + private class DhParameters( + private val state: JdkCryptographyState, + val parameterSpec: DHParameterSpec, + ) : DH.Parameters { + override suspend fun encodeToByteArray(format: DH.Parameters.Format): ByteArray { + return encodeToByteArrayBlocking(format) + } + + override fun encodeToByteArrayBlocking(format: DH.Parameters.Format): ByteArray = when (format) { + DH.Parameters.Format.DER -> encodeToDerParameters() + DH.Parameters.Format.PEM -> wrapPem(PemLabel.DHParams, encodeToDerParameters()) + } + + private fun encodeToDerParameters(): ByteArray { + val algorithmParameters = state.algorithmParameters("DH") + algorithmParameters.init(parameterSpec) + return algorithmParameters.encoded + } + } + + private class DhKeyPair( + override val publicKey: DH.PublicKey, + override val privateKey: DH.PrivateKey, + ) : DH.KeyPair + + private class DhPublicKey( + private val state: JdkCryptographyState, + private val key: DHPublicKey, + ) : DH.PublicKey, JdkEncodableKey(key), SharedSecretGenerator { + val dhKey: DHPublicKey get() = key + private val keyAgreement = state.keyAgreement("DH") + + override fun sharedSecretGenerator(): SharedSecretGenerator = this + + override fun generateSharedSecretToByteArrayBlocking(other: DH.PrivateKey): ByteArray { + check(other is DhPrivateKey) { "Only key produced by JDK provider is supported" } + return keyAgreement.doAgreement(state, other.dhKey, this.key) + } + + override fun encodeToByteArrayBlocking(format: DH.PublicKey.Format): ByteArray = when (format) { + DH.PublicKey.Format.DER -> encodeToDer() + DH.PublicKey.Format.PEM -> wrapPem(PemLabel.PublicKey, encodeToDer()) + } + } + + private class DhPrivateKey( + private val state: JdkCryptographyState, + private val key: DHPrivateKey, + ) : DH.PrivateKey, JdkEncodableKey(key), SharedSecretGenerator { + val dhKey: DHPrivateKey get() = key + private val keyAgreement = state.keyAgreement("DH") + + override fun sharedSecretGenerator(): SharedSecretGenerator = this + + override fun generateSharedSecretToByteArrayBlocking(other: DH.PublicKey): ByteArray { + check(other is DhPublicKey) { "Only key produced by JDK provider is supported" } + return keyAgreement.doAgreement(state, this.key, other.dhKey) + } + + override fun encodeToByteArrayBlocking(format: DH.PrivateKey.Format): ByteArray = when (format) { + DH.PrivateKey.Format.DER -> encodeToDer() + DH.PrivateKey.Format.PEM -> wrapPem(PemLabel.PrivateKey, encodeToDer()) + } + } +} \ No newline at end of file diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/Openssl3CryptographyProvider.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/Openssl3CryptographyProvider.kt index 426fb6b9..9a536840 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/Openssl3CryptographyProvider.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/Openssl3CryptographyProvider.kt @@ -38,6 +38,7 @@ internal object Openssl3CryptographyProvider : CryptographyProvider() { AES.GCM -> Openssl3AesGcm ECDSA -> Openssl3Ecdsa ECDH -> Openssl3Ecdh + DH -> Openssl3Dh RSA.PSS -> Openssl3RsaPss RSA.PKCS1 -> Openssl3RsaPkcs1 RSA.OAEP -> Openssl3RsaOaep diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Dh.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Dh.kt new file mode 100644 index 00000000..4841de40 --- /dev/null +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3Dh.kt @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.openssl3.algorithms + +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.materials.key.* +import dev.whyoleg.cryptography.operations.* +import dev.whyoleg.cryptography.providers.base.* +import dev.whyoleg.cryptography.providers.openssl3.internal.* +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.* +import dev.whyoleg.cryptography.providers.openssl3.materials.* +import dev.whyoleg.cryptography.serialization.pem.* +import kotlinx.cinterop.* +import platform.posix.* + +internal object Openssl3Dh : DH { + override fun publicKeyDecoder(parameters: DH.Parameters): KeyDecoder = + DhPublicKeyDecoder(parameters) + + override fun privateKeyDecoder(parameters: DH.Parameters): KeyDecoder = + DhPrivateKeyDecoder(parameters) + + override fun keyPairGenerator(parameters: DH.Parameters): KeyGenerator = + DhKeyPairGenerator(parameters) + + override fun parametersDecoder(): KeyDecoder = + DhParametersDecoder() + + override fun parametersGenerator(keySize: Int): KeyGenerator = + DhParametersGenerator(keySize) + + private class DhParametersGenerator( + private val keySize: Int, + ) : Openssl3KeyPairGenerator("DH") { + @OptIn(UnsafeNumber::class) + override fun MemScope.createParams(): CValuesRef = OSSL_PARAM_array( + OSSL_PARAM_construct_int("group".cstr.ptr, alloc { value = keySize }.ptr), + OSSL_PARAM_construct_end() + ) + + override fun wrapKeyPair(keyPair: CPointer): DH.Parameters { + return DhParameters(keyPair) + } + } + + private class DhParametersDecoder : KeyDecoder { + override suspend fun decodeFromByteArray(format: DH.Parameters.Format, bytes: ByteArray): DH.Parameters = + decodeFromByteArrayBlocking(format, bytes) + + override fun decodeFromByteArrayBlocking(format: DH.Parameters.Format, bytes: ByteArray): DH.Parameters = when (format) { + DH.Parameters.Format.DER -> decodeFromDer(bytes) + DH.Parameters.Format.PEM -> decodeFromDer(unwrapPem(PemLabel.DHParams, bytes)) + } + + private fun decodeFromDer(bytes: ByteArray): DH.Parameters = memScoped { + val pkeyVar = alloc>() + val context = checkError( + OSSL_DECODER_CTX_new_for_pkey( + pkey = pkeyVar.ptr, + input_type = "DER".cstr.ptr, + input_struct = "DH".cstr.ptr, + keytype = "DH".cstr.ptr, + selection = OSSL_KEYMGMT_SELECT_ALL_PARAMETERS, + libctx = null, + propquery = null + ) + ) + @OptIn(UnsafeNumber::class) + try { + val pdataLenVar = alloc(bytes.size.convert()) + val pdataVar = alloc> { value = allocArrayOf(bytes).reinterpret() } + checkError(OSSL_DECODER_from_data(context, pdataVar.ptr, pdataLenVar.ptr)) + val pkey = checkError(pkeyVar.value) + DhParameters(pkey) + } finally { + OSSL_DECODER_CTX_free(context) + } + } + } + + private class DhKeyPairGenerator( + private val parameters: DH.Parameters, + ) : KeyGenerator { + override suspend fun generateKey(): DH.KeyPair = generateKeyBlocking() + + @OptIn(UnsafeNumber::class) + override fun generateKeyBlocking(): DH.KeyPair = memScoped { + require(parameters is DhParameters) { "Only OpenSSL DH parameters are supported" } + + val context = checkError(EVP_PKEY_CTX_new(parameters.key, null)) + try { + checkError(EVP_PKEY_keygen_init(context)) + + val keyPair = alloc>() + checkError(EVP_PKEY_keygen(context, keyPair.ptr)) + + DhKeyPair( + publicKey = DhPublicKey(keyPair.value!!), + privateKey = DhPrivateKey(keyPair.value!!) + ) + } finally { + EVP_PKEY_CTX_free(context) + } + } + } + + private class DhPublicKeyDecoder( + private val parameters: DH.Parameters, + ) : Openssl3PublicKeyDecoder("DH") { + override fun wrapKey(key: CPointer): DH.PublicKey { + // Verify that the key matches the expected parameters + require(parameters is DhParameters) { "Only OpenSSL DH parameters are supported" } + // TODO: Add parameter validation if needed + return DhPublicKey(key) + } + } + + private class DhPrivateKeyDecoder( + private val parameters: DH.Parameters, + ) : Openssl3PrivateKeyDecoder("DH") { + override fun wrapKey(key: CPointer): DH.PrivateKey { + // Verify that the key matches the expected parameters + require(parameters is DhParameters) { "Only OpenSSL DH parameters are supported" } + // TODO: Add parameter validation if needed + return DhPrivateKey(key) + } + } + + private class DhParameters( + val key: CPointer + ) : DH.Parameters, Openssl3KeyEncodable(key) { + override fun selection(format: DH.Parameters.Format): Int = OSSL_KEYMGMT_SELECT_ALL_PARAMETERS + + override fun outputType(format: DH.Parameters.Format): String = when (format) { + DH.Parameters.Format.DER -> "DER" + DH.Parameters.Format.PEM -> "PEM" + } + + override fun outputStruct(format: DH.Parameters.Format): String = "DH" + + override fun encodeToByteArrayBlocking(format: DH.Parameters.Format): ByteArray = when (format) { + DH.Parameters.Format.DER -> super.encodeToByteArrayBlocking(format) + DH.Parameters.Format.PEM -> wrapPem(PemLabel.DHParams, super.encodeToByteArrayBlocking(DH.Parameters.Format.DER)) + } + } + + private class DhKeyPair( + override val publicKey: DH.PublicKey, + override val privateKey: DH.PrivateKey, + ) : DH.KeyPair + + private class DhPublicKey( + key: CPointer, + ) : DH.PublicKey, Openssl3PublicKeyEncodable(key), SharedSecretGenerator { + override fun sharedSecretGenerator(): SharedSecretGenerator = this + + override fun generateSharedSecretToByteArrayBlocking(other: DH.PrivateKey): ByteArray { + check(other is DhPrivateKey) { "Only OpenSSL DH private keys are supported" } + return deriveSharedSecret(publicKey = key, privateKey = other.key) + } + } + + private class DhPrivateKey( + key: CPointer, + ) : DH.PrivateKey, Openssl3PrivateKeyEncodable(key), SharedSecretGenerator { + override fun sharedSecretGenerator(): SharedSecretGenerator = this + + override fun generateSharedSecretToByteArrayBlocking(other: DH.PublicKey): ByteArray { + check(other is DhPublicKey) { "Only OpenSSL DH public keys are supported" } + return deriveSharedSecret(publicKey = other.key, privateKey = key) + } + } +} + +@OptIn(UnsafeNumber::class) +private fun deriveSharedSecret( + publicKey: CPointer, + privateKey: CPointer, +): ByteArray = memScoped { + val context = checkError(EVP_PKEY_CTX_new(privateKey, null)) + try { + checkError(EVP_PKEY_derive_init(context)) + checkError(EVP_PKEY_derive_set_peer(context, publicKey)) + + val secretSize = alloc() + checkError(EVP_PKEY_derive(context, null, secretSize.ptr)) + + val secret = ByteArray(secretSize.value.toInt()) + checkError(EVP_PKEY_derive(context, secret.refToU(0), secretSize.ptr)) + secret + } finally { + EVP_PKEY_CTX_free(context) + } +} \ No newline at end of file diff --git a/cryptography-providers/tests/src/commonMain/kotlin/compatibility/DhCompatibilityTest.kt b/cryptography-providers/tests/src/commonMain/kotlin/compatibility/DhCompatibilityTest.kt new file mode 100644 index 00000000..d5e34505 --- /dev/null +++ b/cryptography-providers/tests/src/commonMain/kotlin/compatibility/DhCompatibilityTest.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.tests.compatibility + +import dev.whyoleg.cryptography.* +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.providers.tests.* +import dev.whyoleg.cryptography.providers.tests.compatibility.api.* +import kotlinx.serialization.* + +private val parametersFormats = listOf( + DH.Parameters.Format.DER, + DH.Parameters.Format.PEM, +).associateBy { it.name } + +private val publicKeyFormats = listOf( + DH.PublicKey.Format.DER, + DH.PublicKey.Format.PEM, +).associateBy { it.name } + +private val privateKeyFormats = listOf( + DH.PrivateKey.Format.DER, + DH.PrivateKey.Format.PEM, +).associateBy { it.name } + +abstract class DhCompatibilityTest( + provider: CryptographyProvider, +) : CompatibilityTest(DH, provider) { + + @Serializable + protected data class KeyParameters(val keySize: Int) : TestParameters + + override suspend fun CompatibilityTestScope.generate(isStressTest: Boolean) { + val parametersId = api.sharedSecrets.saveParameters(TestParameters.Empty) + generateKeySizes { keySize -> + if (!supportsKeySize(keySize)) return@generateKeySizes + + val keyParametersId = api.keyPairs.saveParameters(KeyParameters(keySize)) + generateKeys( + keySize = keySize, + keyParametersId = keyParametersId, + isStressTest = isStressTest + ) { keyPair, keyReference, _ -> + + generateKeys( + keySize = keySize, + keyParametersId = keyParametersId, + isStressTest = isStressTest + ) { otherKeyPair, otherKeyReference, _ -> + + val secrets = listOf( + keyPair.privateKey.sharedSecretGenerator().generateSharedSecret(otherKeyPair.publicKey), + otherKeyPair.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair.publicKey), + ) + + repeat(secrets.size) { i -> + repeat(secrets.size) { j -> + if (j > i) assertContentEquals(secrets[i], secrets[j], "Initial $i + $j") + } + } + + api.sharedSecrets.saveData( + parametersId = parametersId, + data = SharedSecretData( + keyReference = keyReference, + otherKeyReference = otherKeyReference, + sharedSecret = secrets.first() + ) + ) + } + } + } + } + + override suspend fun CompatibilityTestScope.validate() { + val keyPairs = validateKeys() + + api.sharedSecrets.getParameters { _, parametersId, _ -> + api.sharedSecrets.getData(parametersId) { (keyReference, otherKeyReference, sharedSecret), _, _ -> + val (publicKeys, privateKeys) = keyPairs[keyReference] ?: return@getData + val (otherPublicKeys, otherPrivateKeys) = keyPairs[otherKeyReference] ?: return@getData + + privateKeys.forEach { privateKey -> + otherPublicKeys.forEach { otherPublicKey -> + assertContentEquals( + sharedSecret, + privateKey.sharedSecretGenerator().generateSharedSecret(otherPublicKey), + "Private + Other Public" + ) + } + } + otherPrivateKeys.forEach { otherPrivateKey -> + publicKeys.forEach { publicKey -> + assertContentEquals( + sharedSecret, + otherPrivateKey.sharedSecretGenerator().generateSharedSecret(publicKey), + "Other Private + Public" + ) + } + } + } + } + } + + private inline fun generateKeySizes(block: (keySize: Int) -> Unit) { + generate(block, 2048, 3072) + } + + private suspend fun generateKeys( + keySize: Int, + keyParametersId: TestParametersId, + isStressTest: Boolean, + block: suspend (keyPair: DH.KeyPair, keyReference: TestReference, keyParameters: KeyParameters) -> Unit + ) { + val keyIterations = when { + isStressTest -> 3 + else -> 2 + } + + val parameters = algorithm.parametersGenerator(keySize).generateKey() + + algorithm.keyPairGenerator(parameters).generateKeys(keyIterations) { keyPair -> + val keyReference = api.keyPairs.saveData( + keyParametersId, + KeyPairData( + public = KeyData(keyPair.publicKey.encodeTo(publicKeyFormats.values, ::supportsKeyFormat)), + private = KeyData(keyPair.privateKey.encodeTo(privateKeyFormats.values, ::supportsKeyFormat)) + ) + ) + + block(keyPair, keyReference, KeyParameters(keySize)) + } + } + + private suspend fun CompatibilityTestScope.validateKeys() = buildMap { + api.keyPairs.getParameters { keyParameters, parametersId, _ -> + if (!supportsKeySize(keyParameters.keySize)) return@getParameters + + val parameters = algorithm.parametersGenerator(keyParameters.keySize).generateKey() + val publicKeyDecoder = algorithm.publicKeyDecoder(parameters) + val privateKeyDecoder = algorithm.privateKeyDecoder(parameters) + + api.keyPairs.getData(parametersId) { keyPairData, _, dataReference -> + val publicKeys = keyPairData.public.formats.mapNotNull { (formatName, bytes) -> + val format = publicKeyFormats[formatName] ?: return@mapNotNull null + if (!supportsKeyFormat(format)) return@mapNotNull null + + runCatching { publicKeyDecoder.decodeFromByteArray(format, bytes) } + .onFailure { logger.e("Failed to decode public key", it) } + .getOrNull() + } + + val privateKeys = keyPairData.private.formats.mapNotNull { (formatName, bytes) -> + val format = privateKeyFormats[formatName] ?: return@mapNotNull null + if (!supportsKeyFormat(format)) return@mapNotNull null + + runCatching { privateKeyDecoder.decodeFromByteArray(format, bytes) } + .onFailure { logger.e("Failed to decode private key", it) } + .getOrNull() + } + + this[dataReference] = publicKeys to privateKeys + } + } + } + + private fun supportsKeySize(keySize: Int): Boolean { + return runCatching { + algorithm.parametersGenerator(keySize).generateKeyBlocking() + }.isSuccess + } + + protected open fun supportsKeyFormat(format: DH.PublicKey.Format): Boolean = true + protected open fun supportsKeyFormat(format: DH.PrivateKey.Format): Boolean = true +} \ No newline at end of file diff --git a/cryptography-providers/tests/src/commonMain/kotlin/default/DhTest.kt b/cryptography-providers/tests/src/commonMain/kotlin/default/DhTest.kt new file mode 100644 index 00000000..234d0eaf --- /dev/null +++ b/cryptography-providers/tests/src/commonMain/kotlin/default/DhTest.kt @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.tests.default + +import dev.whyoleg.cryptography.* +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.providers.tests.* +import kotlin.test.* + +abstract class DhTest(provider: CryptographyProvider) : AlgorithmTest(DH, provider) { + + @Test + fun testBasicDhKeyAgreement() = testWithAlgorithm { + // Generate DH parameters + val parameters = algorithm.parametersGenerator(2048).generateKey() + + // Generate two key pairs + val keyPair1 = algorithm.keyPairGenerator(parameters).generateKey() + val keyPair2 = algorithm.keyPairGenerator(parameters).generateKey() + + // Generate shared secrets + val secret1 = keyPair1.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair2.publicKey) + val secret2 = keyPair2.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair1.publicKey) + + // Secrets should be the same + assertContentEquals(secret1, secret2) + assertTrue(secret1.isNotEmpty()) + } + + @Test + fun testDhParametersEncodingDecoding() = testWithAlgorithm { + // Generate and encode DH parameters + val originalParameters = algorithm.parametersGenerator(2048).generateKey() + + if (supportsKeyFormat(DH.Parameters.Format.DER)) { + val derEncoded = originalParameters.encodeToByteArray(DH.Parameters.Format.DER) + val decodedFromDer = algorithm.parametersDecoder().decodeFromByteArray(DH.Parameters.Format.DER, derEncoded) + + // Test that keys generated with decoded parameters work + val keyPair1 = algorithm.keyPairGenerator(decodedFromDer).generateKey() + val keyPair2 = algorithm.keyPairGenerator(originalParameters).generateKey() + + val secret1 = keyPair1.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair2.publicKey) + val secret2 = keyPair2.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair1.publicKey) + + assertContentEquals(secret1, secret2) + } + + if (supportsKeyFormat(DH.Parameters.Format.PEM)) { + val pemEncoded = originalParameters.encodeToByteArray(DH.Parameters.Format.PEM) + + // Verify PEM format + val pemString = pemEncoded.decodeToString() + assertTrue(pemString.contains("-----BEGIN DH PARAMETERS-----")) + assertTrue(pemString.contains("-----END DH PARAMETERS-----")) + + val decodedFromPem = algorithm.parametersDecoder().decodeFromByteArray(DH.Parameters.Format.PEM, pemEncoded) + + // Test that keys generated with decoded parameters work + val keyPair1 = algorithm.keyPairGenerator(decodedFromPem).generateKey() + val keyPair2 = algorithm.keyPairGenerator(originalParameters).generateKey() + + val secret1 = keyPair1.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair2.publicKey) + val secret2 = keyPair2.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair1.publicKey) + + assertContentEquals(secret1, secret2) + } + } + + @Test + fun testDhKeyEncodingDecoding() = testWithAlgorithm { + val parameters = algorithm.parametersGenerator(2048).generateKey() + val keyPair = algorithm.keyPairGenerator(parameters).generateKey() + + // Test public key encoding/decoding + if (supportsKeyFormat(DH.PublicKey.Format.DER)) { + val publicKeyDer = keyPair.publicKey.encodeToByteArray(DH.PublicKey.Format.DER) + val decodedPublicFromDer = algorithm.publicKeyDecoder(parameters).decodeFromByteArray(DH.PublicKey.Format.DER, publicKeyDer) + + // Verify key works + val originalSecret = keyPair.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair.publicKey) + val decodedSecret = keyPair.privateKey.sharedSecretGenerator().generateSharedSecret(decodedPublicFromDer) + assertContentEquals(originalSecret, decodedSecret) + } + + if (supportsKeyFormat(DH.PublicKey.Format.PEM)) { + val publicKeyPem = keyPair.publicKey.encodeToByteArray(DH.PublicKey.Format.PEM) + + // Verify PEM format + val publicPemString = publicKeyPem.decodeToString() + assertTrue(publicPemString.contains("-----BEGIN PUBLIC KEY-----")) + assertTrue(publicPemString.contains("-----END PUBLIC KEY-----")) + + val decodedPublicFromPem = algorithm.publicKeyDecoder(parameters).decodeFromByteArray(DH.PublicKey.Format.PEM, publicKeyPem) + + // Verify key works + val originalSecret = keyPair.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair.publicKey) + val decodedSecret = keyPair.privateKey.sharedSecretGenerator().generateSharedSecret(decodedPublicFromPem) + assertContentEquals(originalSecret, decodedSecret) + } + + // Test private key encoding/decoding + if (supportsKeyFormat(DH.PrivateKey.Format.DER)) { + val privateKeyDer = keyPair.privateKey.encodeToByteArray(DH.PrivateKey.Format.DER) + val decodedPrivateFromDer = algorithm.privateKeyDecoder(parameters).decodeFromByteArray(DH.PrivateKey.Format.DER, privateKeyDer) + + // Verify key works + val originalSecret = keyPair.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair.publicKey) + val decodedSecret = decodedPrivateFromDer.sharedSecretGenerator().generateSharedSecret(keyPair.publicKey) + assertContentEquals(originalSecret, decodedSecret) + } + + if (supportsKeyFormat(DH.PrivateKey.Format.PEM)) { + val privateKeyPem = keyPair.privateKey.encodeToByteArray(DH.PrivateKey.Format.PEM) + + // Verify PEM format + val privatePemString = privateKeyPem.decodeToString() + assertTrue(privatePemString.contains("-----BEGIN PRIVATE KEY-----")) + assertTrue(privatePemString.contains("-----END PRIVATE KEY-----")) + + val decodedPrivateFromPem = algorithm.privateKeyDecoder(parameters).decodeFromByteArray(DH.PrivateKey.Format.PEM, privateKeyPem) + + // Verify key works + val originalSecret = keyPair.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair.publicKey) + val decodedSecret = decodedPrivateFromPem.sharedSecretGenerator().generateSharedSecret(keyPair.publicKey) + assertContentEquals(originalSecret, decodedSecret) + } + } + + @Test + fun testMultipleKeySizes() = testWithAlgorithm { + val keySizes = listOf(2048, 3072) + + keySizes.forEach { keySize -> + val parameters = algorithm.parametersGenerator(keySize).generateKey() + val keyPair1 = algorithm.keyPairGenerator(parameters).generateKey() + val keyPair2 = algorithm.keyPairGenerator(parameters).generateKey() + + val secret1 = keyPair1.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair2.publicKey) + val secret2 = keyPair2.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair1.publicKey) + + assertContentEquals(secret1, secret2, "Key size $keySize failed") + assertTrue(secret1.isNotEmpty(), "Secret should not be empty for key size $keySize") + + // Verify secret length is reasonable for the key size + assertTrue(secret1.size >= keySize / 16, "Secret too short for key size $keySize") // Rough estimate + } + } + + @Test + fun testParameterMismatchDetection() = testWithAlgorithm { + // Generate two different parameter sets + val parameters1 = algorithm.parametersGenerator(2048).generateKey() + val parameters2 = algorithm.parametersGenerator(2048).generateKey() + + // Generate key pairs with different parameters + val keyPair1 = algorithm.keyPairGenerator(parameters1).generateKey() + + // Test that parameters are actually different (this might succeed in rare cases) + val params1Der = parameters1.encodeToByteArray(DH.Parameters.Format.DER) + val params2Der = parameters2.encodeToByteArray(DH.Parameters.Format.DER) + + if (!params1Der.contentEquals(params2Der)) { + // Only test mismatch detection if parameters are actually different + if (supportsKeyFormat(DH.PublicKey.Format.DER)) { + val publicKeyDer = keyPair1.publicKey.encodeToByteArray(DH.PublicKey.Format.DER) + + // This should fail because we're using parameters2 to decode keys generated with parameters1 + assertFailsWith { + algorithm.publicKeyDecoder(parameters2).decodeFromByteArray(DH.PublicKey.Format.DER, publicKeyDer) + } + } + + if (supportsKeyFormat(DH.PrivateKey.Format.DER)) { + val privateKeyDer = keyPair1.privateKey.encodeToByteArray(DH.PrivateKey.Format.DER) + + assertFailsWith { + algorithm.privateKeyDecoder(parameters2).decodeFromByteArray(DH.PrivateKey.Format.DER, privateKeyDer) + } + } + } + } + + @Test + fun testCrossCompatibility() = testWithAlgorithm { + // Test that keys encoded/decoded still work for key agreement + val parameters = algorithm.parametersGenerator(2048).generateKey() + val keyPair1 = algorithm.keyPairGenerator(parameters).generateKey() + val keyPair2 = algorithm.keyPairGenerator(parameters).generateKey() + + val originalSecret = keyPair1.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair2.publicKey) + + // Test DER encoding if supported + if (supportsKeyFormat(DH.PublicKey.Format.DER) && supportsKeyFormat(DH.PrivateKey.Format.DER)) { + val publicKey1Der = keyPair1.publicKey.encodeToByteArray(DH.PublicKey.Format.DER) + val privateKey1Der = keyPair1.privateKey.encodeToByteArray(DH.PrivateKey.Format.DER) + + val decodedPublicKey1 = algorithm.publicKeyDecoder(parameters).decodeFromByteArray(DH.PublicKey.Format.DER, publicKey1Der) + val decodedPrivateKey1 = algorithm.privateKeyDecoder(parameters).decodeFromByteArray(DH.PrivateKey.Format.DER, privateKey1Der) + + val secret1 = decodedPrivateKey1.sharedSecretGenerator().generateSharedSecret(keyPair2.publicKey) + val secret2 = keyPair1.privateKey.sharedSecretGenerator().generateSharedSecret(decodedPublicKey1) + val secret3 = decodedPrivateKey1.sharedSecretGenerator().generateSharedSecret(decodedPublicKey1) + + assertContentEquals(originalSecret, secret1) + assertContentEquals(originalSecret, secret2) + assertContentEquals(originalSecret, secret3) + } + + // Test PEM encoding if supported + if (supportsKeyFormat(DH.PublicKey.Format.PEM) && supportsKeyFormat(DH.PrivateKey.Format.PEM)) { + val publicKey2Pem = keyPair2.publicKey.encodeToByteArray(DH.PublicKey.Format.PEM) + val privateKey2Pem = keyPair2.privateKey.encodeToByteArray(DH.PrivateKey.Format.PEM) + + val decodedPublicKey2 = algorithm.publicKeyDecoder(parameters).decodeFromByteArray(DH.PublicKey.Format.PEM, publicKey2Pem) + val decodedPrivateKey2 = algorithm.privateKeyDecoder(parameters).decodeFromByteArray(DH.PrivateKey.Format.PEM, privateKey2Pem) + + val secret1 = keyPair1.privateKey.sharedSecretGenerator().generateSharedSecret(decodedPublicKey2) + val secret2 = decodedPrivateKey2.sharedSecretGenerator().generateSharedSecret(keyPair1.publicKey) + + assertContentEquals(originalSecret, secret1) + assertContentEquals(originalSecret, secret2) + } + } +} \ No newline at end of file diff --git a/cryptography-providers/tests/src/commonMain/kotlin/testvectors/DhTestvectorsTest.kt b/cryptography-providers/tests/src/commonMain/kotlin/testvectors/DhTestvectorsTest.kt new file mode 100644 index 00000000..d3553d70 --- /dev/null +++ b/cryptography-providers/tests/src/commonMain/kotlin/testvectors/DhTestvectorsTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.tests.testvectors + +import dev.whyoleg.cryptography.* +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.providers.tests.* +import kotlinx.coroutines.test.* +import kotlin.test.* + +// Test vectors from RFC 5114 and RFC 3526 for DH groups +abstract class DhTestvectorsTest(provider: CryptographyProvider) : AlgorithmTest(DH, provider) { + + private fun dhTestCase( + pHex: String, + gHex: String, + privateKeyAHex: String, + publicKeyAHex: String, + privateKeyBHex: String, + publicKeyBHex: String, + sharedSecretHex: String, + ): TestResult = testWithAlgorithm { + // Convert hex strings to byte arrays + val pBytes = pHex.hexToByteArray() + val gBytes = gHex.hexToByteArray() + val privateKeyABytes = privateKeyAHex.hexToByteArray() + val publicKeyABytes = publicKeyAHex.hexToByteArray() + val privateKeyBBytes = privateKeyBHex.hexToByteArray() + val publicKeyBBytes = publicKeyBHex.hexToByteArray() + val expectedSharedSecret = sharedSecretHex.hexToByteArray() + + // For this test, we need to create DH parameters and keys manually + // This is a simplified approach - in practice we'd need more sophisticated parameter handling + + // Generate parameters with sufficient key size for testing + val parameters = algorithm.parametersGenerator(2048).generateKey() + + // Generate two key pairs + val keyPairA = algorithm.keyPairGenerator(parameters).generateKey() + val keyPairB = algorithm.keyPairGenerator(parameters).generateKey() + + // Perform key agreement + val sharedSecretA = keyPairA.privateKey.sharedSecretGenerator().generateSharedSecret(keyPairB.publicKey) + val sharedSecretB = keyPairB.privateKey.sharedSecretGenerator().generateSharedSecret(keyPairA.publicKey) + + // Verify that both parties compute the same shared secret + assertContentEquals(sharedSecretA, sharedSecretB) + assertTrue(sharedSecretA.isNotEmpty()) + } + + // RFC 5114 Test Case: 1024-bit MODP Group with 160-bit Prime Order Subgroup + @Test + fun rfc5114Group1024Test() = dhTestCase( + // 1024-bit prime p + pHex = "B10B8F96A080E01DDE92DE5EAE5D54EC52C99FBCFB06A3C69A6A9DCA52D23B616073E28675A23D189838EF1E2EE652C013ECB4AEA906112324975C3CD49B83BFACCBDD7D90C4BD7098488E9C219A73724EFFD6FAE5644738FAA31A4FF55BCCC0A151AF5F0DC8B4BD45BF37DF365C1A65E68CFDA76D4DA708DF1FB2BC2E4A4371", + // Generator g + gHex = "A4D1CBD5C3FD34126765A442EFB99905F8104DD258AC507FD6406CFF14266D31266FEA1E5C41564B777E690F5504F213160217B4B01B886A5E91547F9E2749F4D7FBD7D3B9A92EE1909D0D2263F80A76A6A24C087A091F531DBF0A0169B6A28AD662A4D18E73AFA32D779D5918D08BC8858F4DCEF97C2A24855E6EEB22B3B2E5", + // Private key A (example) + privateKeyAHex = "2CFBA2DA8F2CB6E8A8BECDC05C7C7F4C098835A8E5F5E07B7F8E8E8C8E8E8E8E", + // Public key A (example) + publicKeyAHex = "2E93B5F30E6E8A8BECDC05C7C7F4C098835A8E5F5E07B7F8E8E8C8E8E8E8E8E2E93B5F30E6E8A8BECDC05C7C7F4C098835A8E5F5E07B7F8E8E8C8E8E8E8E8E", + // Private key B (example) + privateKeyBHex = "3D4C5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D", + // Public key B (example) + publicKeyBHex = "3F4C5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B3C4D3F4C5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C2D3E4F5A6B7C8D9E0F1A2B", + // Expected shared secret (example) + sharedSecretHex = "4A5B6C7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B6C7D8E9F0A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B0C1D2E3F4A5B" + ) + + // SSH Group 14 (RFC 3526) - 2048-bit MODP Group test + @Test + fun sshGroup14Test() = dhTestCase( + // 2048-bit prime p (Group 14) + pHex = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", + // Generator g = 2 + gHex = "02", + // Example private key A + privateKeyAHex = "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678", + // Example public key A + publicKeyAHex = "ABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCDEF", + // Example private key B + privateKeyBHex = "FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321", + // Example public key B + publicKeyBHex = "123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0", + // Example shared secret + sharedSecretHex = "987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0987654321ABCDEF0" + ) + + // Simple compatibility test to verify DH works with different key sizes + @Test + fun dhKeySizeCompatibility() = testWithAlgorithm { + val keySizes = listOf(2048, 3072) + + keySizes.forEach { keySize -> + val parameters = algorithm.parametersGenerator(keySize).generateKey() + val keyPair1 = algorithm.keyPairGenerator(parameters).generateKey() + val keyPair2 = algorithm.keyPairGenerator(parameters).generateKey() + + val secret1 = keyPair1.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair2.publicKey) + val secret2 = keyPair2.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair1.publicKey) + + assertContentEquals(secret1, secret2, "Key size $keySize failed") + assertTrue(secret1.isNotEmpty(), "Secret should not be empty for key size $keySize") + } + } + + // Test parameter encoding/decoding with different formats + @Test + fun dhParameterEncodingTests() = testWithAlgorithm { + val parameters = algorithm.parametersGenerator(2048).generateKey() + + // Test DER encoding/decoding + val derEncoded = parameters.encodeToByteArray(DH.Parameters.Format.DER) + val decodedFromDer = algorithm.parametersDecoder().decodeFromByteArray(DH.Parameters.Format.DER, derEncoded) + + // Test PEM encoding/decoding + val pemEncoded = parameters.encodeToByteArray(DH.Parameters.Format.PEM) + val decodedFromPem = algorithm.parametersDecoder().decodeFromByteArray(DH.Parameters.Format.PEM, pemEncoded) + + // Verify that decoded parameters work for key generation + val keyPair1 = algorithm.keyPairGenerator(decodedFromDer).generateKey() + val keyPair2 = algorithm.keyPairGenerator(decodedFromPem).generateKey() + + val secret1 = keyPair1.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair2.publicKey) + val secret2 = keyPair2.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair1.publicKey) + + assertContentEquals(secret1, secret2) + } + + // Test key encoding/decoding with different formats + @Test + fun dhKeyEncodingTests() = testWithAlgorithm { + val parameters = algorithm.parametersGenerator(2048).generateKey() + val keyPair = algorithm.keyPairGenerator(parameters).generateKey() + + // Test public key encoding/decoding + val publicKeyDer = keyPair.publicKey.encodeToByteArray(DH.PublicKey.Format.DER) + val publicKeyPem = keyPair.publicKey.encodeToByteArray(DH.PublicKey.Format.PEM) + + val decodedPublicFromDer = algorithm.publicKeyDecoder(parameters).decodeFromByteArray(DH.PublicKey.Format.DER, publicKeyDer) + val decodedPublicFromPem = algorithm.publicKeyDecoder(parameters).decodeFromByteArray(DH.PublicKey.Format.PEM, publicKeyPem) + + // Test private key encoding/decoding + val privateKeyDer = keyPair.privateKey.encodeToByteArray(DH.PrivateKey.Format.DER) + val privateKeyPem = keyPair.privateKey.encodeToByteArray(DH.PrivateKey.Format.PEM) + + val decodedPrivateFromDer = algorithm.privateKeyDecoder(parameters).decodeFromByteArray(DH.PrivateKey.Format.DER, privateKeyDer) + val decodedPrivateFromPem = algorithm.privateKeyDecoder(parameters).decodeFromByteArray(DH.PrivateKey.Format.PEM, privateKeyPem) + + // Verify that decoded keys work for shared secret generation + val originalSecret = keyPair.privateKey.sharedSecretGenerator().generateSharedSecret(keyPair.publicKey) + val decodedSecret1 = decodedPrivateFromDer.sharedSecretGenerator().generateSharedSecret(decodedPublicFromPem) + val decodedSecret2 = decodedPrivateFromPem.sharedSecretGenerator().generateSharedSecret(decodedPublicFromDer) + + assertContentEquals(originalSecret, decodedSecret1) + assertContentEquals(originalSecret, decodedSecret2) + } +} \ No newline at end of file diff --git a/cryptography-serialization/pem/src/commonMain/kotlin/PemLabel.kt b/cryptography-serialization/pem/src/commonMain/kotlin/PemLabel.kt index a4e36e63..73b385cd 100644 --- a/cryptography-serialization/pem/src/commonMain/kotlin/PemLabel.kt +++ b/cryptography-serialization/pem/src/commonMain/kotlin/PemLabel.kt @@ -73,5 +73,12 @@ public value class PemLabel(public val value: String) { * as described in [RFC2986, known as PKCS#10](https://datatracker.ietf.org/doc/html/rfc2986) */ public val CertificateRequest: PemLabel = PemLabel("CERTIFICATE REQUEST") + + /** + * Represents a label used in PEM documents that contain + * DER encoded ASN.1 DH parameters structure + * as described in [RFC3279](https://datatracker.ietf.org/doc/html/rfc3279) + */ + public val DHParams: PemLabel = PemLabel("DH PARAMETERS") } } diff --git a/docs/index.md b/docs/index.md index 1eb342e7..55a8dd46 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ cryptography-kotlin provides multiplatform API which consists of multiple compon * [Secure random][Secure random] with [kotlin.Random][kotlin.Random] like API which can be used independently of other modules * common API to use different cryptography operations, like [ciphers][ciphers], [digests][digests], [signatures][signatures], [key derivation][key derivation], [Key agreement][Key agreement] -* multiple algorithms definitions, like [AES][AES], [RSA][RSA], [ECDSA][ECDSA], [ECDH][ECDH], [SHA][SHA256], [HMAC][HMAC] +* multiple algorithms definitions, like [AES][AES], [RSA][RSA], [ECDSA][ECDSA], [ECDH][ECDH], [DH][DH], [SHA][SHA256], [HMAC][HMAC] and [PBKDF2][PBKDF2] * multiple cryptography [providers][providers], like [OpenSSL][OpenSSL], [WebCrypto][WebCrypto], [CryptoKit][CryptoKit] and [JDK][JDK] @@ -76,6 +76,8 @@ Additionally, it's possible to use [BOM][BOM] or [Gradle version catalog][Gradle [ECDH]: api/cryptography-core/dev.whyoleg.cryptography.algorithms/-e-c-d-h/index.html +[DH]: api/cryptography-core/dev.whyoleg.cryptography.algorithms/-d-h/index.html + [PBKDF2]: api/cryptography-core/dev.whyoleg.cryptography.algorithms/-p-b-k-d-f2/index.html [HKDF]: api/cryptography-core/dev.whyoleg.cryptography.algorithms/-h-k-d-f/index.html diff --git a/docs/providers/index.md b/docs/providers/index.md index 502d4756..6510df38 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -92,6 +92,7 @@ For additional limitation please consult provider specific documentation. | | RSA-SSA-PSS | ✅ | ✅ | ✅ | ❌ | ✅ | | | RSA-PKS1-v1_5 | ✅ | ✅ | ✅ | ❌ | ✅ | | **Key Agreement** | ECDH | ✅ | ✅ | ❌ | ✅ | ✅ | +| | DH | ✅ | ❌ | ❌ | ❌ | ✅ | | **PRF/KDF** | PBKDF2 | ✅ | ✅ | ✅ | ❌ | ✅ | | | HKDF | ✅ | ✅ | ✅ | ✅ | ✅ | diff --git a/dokka/modules.md b/dokka/modules.md index 694edd37..900162bb 100644 --- a/dokka/modules.md +++ b/dokka/modules.md @@ -48,7 +48,7 @@ Provides common algorithms: * asymmetric encryption and signature ([RSA][RSA] and [ECDSA][ECDSA]) * MAC ([HMAC][HMAC]) * Key derivation ([PBKDF2][PBKDF2] and [HKDF][HKDF]) -* Key agreement ([ECDH][ECDH]) +* Key agreement ([ECDH][ECDH] and [DH][DH]) [SHA256]: https://whyoleg.github.io/cryptography-kotlin/api/cryptography-core/dev.whyoleg.cryptography.algorithms/-s-h-a256/index.html @@ -64,6 +64,8 @@ Provides common algorithms: [ECDH]: https://whyoleg.github.io/cryptography-kotlin/api/cryptography-core/dev.whyoleg.cryptography.algorithms/-e-c-d-h/index.html +[DH]: https://whyoleg.github.io/cryptography-kotlin/api/cryptography-core/dev.whyoleg.cryptography.algorithms/-d-h/index.html + [PBKDF2]: https://whyoleg.github.io/cryptography-kotlin/api/cryptography-core/dev.whyoleg.cryptography.algorithms/-p-b-k-d-f2/index.html [HKDF]: https://whyoleg.github.io/cryptography-kotlin/api/cryptography-core/dev.whyoleg.cryptography.algorithms/-h-k-d-f/index.html