From 6028f5d48cc3900793757c8b99839646e996003d Mon Sep 17 00:00:00 2001 From: Aria Wisp Date: Sat, 6 Sep 2025 19:57:19 -0600 Subject: [PATCH 1/2] webcrypto: implement EdDSA and XDH; internal algorithms wiring; provider updates --- .../kotlin/WebCryptoCryptographyProvider.kt | 2 + .../kotlin/algorithms/WebCryptoEdDSA.kt | 106 ++++++++++++++ .../kotlin/algorithms/WebCryptoXDH.kt | 135 ++++++++++++++++++ .../commonMain/kotlin/internal/Algorithms.kt | 3 +- .../jsMain/kotlin/internal/Algorithms.js.kt | 9 +- .../kotlin/internal/Algorithms.wasmJs.kt | 9 +- 6 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoEdDSA.kt create mode 100644 cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoXDH.kt diff --git a/cryptography-providers/webcrypto/src/commonMain/kotlin/WebCryptoCryptographyProvider.kt b/cryptography-providers/webcrypto/src/commonMain/kotlin/WebCryptoCryptographyProvider.kt index d92b5908..c43f7340 100644 --- a/cryptography-providers/webcrypto/src/commonMain/kotlin/WebCryptoCryptographyProvider.kt +++ b/cryptography-providers/webcrypto/src/commonMain/kotlin/WebCryptoCryptographyProvider.kt @@ -29,7 +29,9 @@ internal object WebCryptoCryptographyProvider : CryptographyProvider() { RSA.PSS -> WebCryptoRsaPss RSA.PKCS1 -> WebCryptoRsaPkcs1 ECDSA -> WebCryptoEcdsa + EdDSA -> WebCryptoEdDSA ECDH -> WebCryptoEcdh + XDH -> WebCryptoXDH PBKDF2 -> WebCryptoPbkdf2 HKDF -> WebCryptoHkdf else -> null diff --git a/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoEdDSA.kt b/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoEdDSA.kt new file mode 100644 index 00000000..a4d91dc2 --- /dev/null +++ b/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoEdDSA.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.webcrypto.algorithms + +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.materials.key.* +import dev.whyoleg.cryptography.operations.* +import dev.whyoleg.cryptography.providers.webcrypto.internal.* +import dev.whyoleg.cryptography.providers.webcrypto.materials.* +import dev.whyoleg.cryptography.providers.webcrypto.operations.* +import dev.whyoleg.cryptography.providers.base.materials.* +import dev.whyoleg.cryptography.serialization.pem.* + +internal object WebCryptoEdDSA : EdDSA { + private fun curveName(curve: EdDSA.Curve): String = when (curve) { + EdDSA.Curve.Ed25519 -> "Ed25519" + EdDSA.Curve.Ed448 -> "Ed448" + } + + override fun publicKeyDecoder(curve: EdDSA.Curve): KeyDecoder = WebCryptoKeyDecoder( + algorithm = Algorithm(curveName(curve)), + keyProcessor = EdPublicKeyProcessor, + keyWrapper = WebCryptoKeyWrapper(arrayOf("verify")) { EdDsaPublicKey(it) }, + ) + + override fun privateKeyDecoder(curve: EdDSA.Curve): KeyDecoder = WebCryptoKeyDecoder( + algorithm = Algorithm(curveName(curve)), + keyProcessor = EdPrivateKeyProcessor, + keyWrapper = WebCryptoKeyWrapper(arrayOf("sign")) { EdDsaPrivateKey(it) }, + ) + + override fun keyPairGenerator(curve: EdDSA.Curve): KeyGenerator = WebCryptoAsymmetricKeyGenerator( + algorithm = Algorithm(curveName(curve)), + keyUsages = arrayOf("verify", "sign"), + keyPairWrapper = { EdDsaKeyPair(EdDsaPublicKey(it.publicKey), EdDsaPrivateKey(it.privateKey)) }, + ) + + private class EdDsaKeyPair( + override val publicKey: EdDSA.PublicKey, + override val privateKey: EdDSA.PrivateKey, + ) : EdDSA.KeyPair + + private class EdDsaPublicKey( + val publicKey: CryptoKey, + ) : WebCryptoEncodableKey(publicKey, EdPublicKeyProcessor), EdDSA.PublicKey { + override fun signatureVerifier(): SignatureVerifier { + return WebCryptoSignatureVerifier(publicKey.algorithm, publicKey) + } + } + + private class EdDsaPrivateKey( + val privateKey: CryptoKey, + ) : WebCryptoEncodableKey(privateKey, EdPrivateKeyProcessor), EdDSA.PrivateKey { + override fun signatureGenerator(): SignatureGenerator { + return WebCryptoSignatureGenerator(privateKey.algorithm, privateKey) + } + } +} + +private object EdPublicKeyProcessor : WebCryptoKeyProcessor() { + override fun stringFormat(format: EdDSA.PublicKey.Format): String = when (format) { + EdDSA.PublicKey.Format.JWK -> "jwk" + EdDSA.PublicKey.Format.RAW -> "raw" + EdDSA.PublicKey.Format.DER, + EdDSA.PublicKey.Format.PEM -> "spki" + } + + override fun beforeDecoding(algorithm: Algorithm, format: EdDSA.PublicKey.Format, key: ByteArray): ByteArray = when (format) { + EdDSA.PublicKey.Format.JWK -> key + EdDSA.PublicKey.Format.RAW -> key + EdDSA.PublicKey.Format.DER -> key + EdDSA.PublicKey.Format.PEM -> unwrapPem(PemLabel.PublicKey, key) + } + + override fun afterEncoding(format: EdDSA.PublicKey.Format, key: ByteArray): ByteArray = when (format) { + EdDSA.PublicKey.Format.JWK -> key + EdDSA.PublicKey.Format.RAW -> key + EdDSA.PublicKey.Format.DER -> key + EdDSA.PublicKey.Format.PEM -> wrapPem(PemLabel.PublicKey, key) + } +} + +private object EdPrivateKeyProcessor : WebCryptoKeyProcessor() { + override fun stringFormat(format: EdDSA.PrivateKey.Format): String = when (format) { + EdDSA.PrivateKey.Format.JWK -> "jwk" + EdDSA.PrivateKey.Format.RAW -> "raw" + EdDSA.PrivateKey.Format.DER -> "pkcs8" + EdDSA.PrivateKey.Format.PEM -> "pkcs8" + } + + override fun beforeDecoding(algorithm: Algorithm, format: EdDSA.PrivateKey.Format, key: ByteArray): ByteArray = when (format) { + EdDSA.PrivateKey.Format.JWK -> key + EdDSA.PrivateKey.Format.RAW -> key + EdDSA.PrivateKey.Format.DER -> key + EdDSA.PrivateKey.Format.PEM -> unwrapPem(PemLabel.PrivateKey, key) + } + + override fun afterEncoding(format: EdDSA.PrivateKey.Format, key: ByteArray): ByteArray = when (format) { + EdDSA.PrivateKey.Format.JWK -> key + EdDSA.PrivateKey.Format.RAW -> key + EdDSA.PrivateKey.Format.DER -> key + EdDSA.PrivateKey.Format.PEM -> wrapPem(PemLabel.PrivateKey, key) + } +} diff --git a/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoXDH.kt b/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoXDH.kt new file mode 100644 index 00000000..a6b3e768 --- /dev/null +++ b/cryptography-providers/webcrypto/src/commonMain/kotlin/algorithms/WebCryptoXDH.kt @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.webcrypto.algorithms + +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.materials.key.* +import dev.whyoleg.cryptography.operations.* +import dev.whyoleg.cryptography.providers.webcrypto.internal.* +import dev.whyoleg.cryptography.providers.webcrypto.materials.* +import dev.whyoleg.cryptography.providers.base.materials.* +import dev.whyoleg.cryptography.serialization.pem.* + +internal object WebCryptoXDH : XDH { + private fun curveName(curve: XDH.Curve): String = when (curve) { + XDH.Curve.X25519 -> "X25519" + XDH.Curve.X448 -> "X448" + } + + override fun publicKeyDecoder(curve: XDH.Curve): KeyDecoder = WebCryptoKeyDecoder( + algorithm = Algorithm(curveName(curve)), + keyProcessor = XdhPublicKeyProcessor, + keyWrapper = WebCryptoKeyWrapper(arrayOf()) { XdhPublicKey(it) }, + ) + + override fun privateKeyDecoder(curve: XDH.Curve): KeyDecoder = WebCryptoKeyDecoder( + algorithm = Algorithm(curveName(curve)), + keyProcessor = XdhPrivateKeyProcessor, + keyWrapper = WebCryptoKeyWrapper(arrayOf("deriveBits")) { XdhPrivateKey(it) }, + ) + + override fun keyPairGenerator(curve: XDH.Curve): KeyGenerator = WebCryptoAsymmetricKeyGenerator( + algorithm = Algorithm(curveName(curve)), + keyUsages = arrayOf("deriveBits"), + keyPairWrapper = { XdhKeyPair(XdhPublicKey(it.publicKey), XdhPrivateKey(it.privateKey)) }, + ) + + private class XdhKeyPair( + override val publicKey: XDH.PublicKey, + override val privateKey: XDH.PrivateKey, + ) : XDH.KeyPair + + private class XdhPublicKey( + val publicKey: CryptoKey, + ) : WebCryptoEncodableKey(publicKey, XdhPublicKeyProcessor), XDH.PublicKey, SharedSecretGenerator { + override fun sharedSecretGenerator(): SharedSecretGenerator = this + + private fun deriveLengthBits(): Int = when (publicKey.algorithm.algorithmName) { + "X25519" -> 32 * 8 + "X448" -> 56 * 8 + else -> error("Unknown XDH algorithm: ${publicKey.algorithm.algorithmName}") + } + + override suspend fun generateSharedSecretToByteArray(other: XDH.PrivateKey): ByteArray { + check(other is XdhPrivateKey) + return WebCrypto.deriveBits( + algorithm = KeyDeriveAlgorithm(publicKey.algorithm.algorithmName, publicKey), + baseKey = other.privateKey, + length = deriveLengthBits() + ) + } + + override fun generateSharedSecretToByteArrayBlocking(other: XDH.PrivateKey): ByteArray = nonBlocking() + } + + private class XdhPrivateKey( + val privateKey: CryptoKey, + ) : WebCryptoEncodableKey(privateKey, XdhPrivateKeyProcessor), XDH.PrivateKey, SharedSecretGenerator { + override fun sharedSecretGenerator(): SharedSecretGenerator = this + + private fun deriveLengthBits(): Int = when (privateKey.algorithm.algorithmName) { + "X25519" -> 32 * 8 + "X448" -> 56 * 8 + else -> error("Unknown XDH algorithm: ${privateKey.algorithm.algorithmName}") + } + + override suspend fun generateSharedSecretToByteArray(other: XDH.PublicKey): ByteArray { + check(other is XdhPublicKey) + return WebCrypto.deriveBits( + algorithm = KeyDeriveAlgorithm(privateKey.algorithm.algorithmName, other.publicKey), + baseKey = privateKey, + length = deriveLengthBits() + ) + } + + override fun generateSharedSecretToByteArrayBlocking(other: XDH.PublicKey): ByteArray = nonBlocking() + } +} + +private object XdhPublicKeyProcessor : WebCryptoKeyProcessor() { + override fun stringFormat(format: XDH.PublicKey.Format): String = when (format) { + XDH.PublicKey.Format.JWK -> "jwk" + XDH.PublicKey.Format.RAW -> "raw" + XDH.PublicKey.Format.DER, + XDH.PublicKey.Format.PEM -> "spki" + } + + override fun beforeDecoding(algorithm: Algorithm, format: XDH.PublicKey.Format, key: ByteArray): ByteArray = when (format) { + XDH.PublicKey.Format.JWK -> key + XDH.PublicKey.Format.RAW -> key + XDH.PublicKey.Format.DER -> key + XDH.PublicKey.Format.PEM -> unwrapPem(PemLabel.PublicKey, key) + } + + override fun afterEncoding(format: XDH.PublicKey.Format, key: ByteArray): ByteArray = when (format) { + XDH.PublicKey.Format.JWK -> key + XDH.PublicKey.Format.RAW -> key + XDH.PublicKey.Format.DER -> key + XDH.PublicKey.Format.PEM -> wrapPem(PemLabel.PublicKey, key) + } +} + +private object XdhPrivateKeyProcessor : WebCryptoKeyProcessor() { + override fun stringFormat(format: XDH.PrivateKey.Format): String = when (format) { + XDH.PrivateKey.Format.JWK -> "jwk" + XDH.PrivateKey.Format.RAW -> "raw" + XDH.PrivateKey.Format.DER -> "pkcs8" + XDH.PrivateKey.Format.PEM -> "pkcs8" + } + + override fun beforeDecoding(algorithm: Algorithm, format: XDH.PrivateKey.Format, key: ByteArray): ByteArray = when (format) { + XDH.PrivateKey.Format.JWK -> key + XDH.PrivateKey.Format.RAW -> key + XDH.PrivateKey.Format.DER -> key + XDH.PrivateKey.Format.PEM -> unwrapPem(PemLabel.PrivateKey, key) + } + + override fun afterEncoding(format: XDH.PrivateKey.Format, key: ByteArray): ByteArray = when (format) { + XDH.PrivateKey.Format.JWK -> key + XDH.PrivateKey.Format.RAW -> key + XDH.PrivateKey.Format.DER -> key + XDH.PrivateKey.Format.PEM -> wrapPem(PemLabel.PrivateKey, key) + } +} diff --git a/cryptography-providers/webcrypto/src/commonMain/kotlin/internal/Algorithms.kt b/cryptography-providers/webcrypto/src/commonMain/kotlin/internal/Algorithms.kt index d1d0ef03..2007d2b2 100644 --- a/cryptography-providers/webcrypto/src/commonMain/kotlin/internal/Algorithms.kt +++ b/cryptography-providers/webcrypto/src/commonMain/kotlin/internal/Algorithms.kt @@ -30,7 +30,7 @@ internal expect val Algorithm.ecKeyAlgorithmNamedCurve: String internal expect fun EcdsaSignatureAlgorithm(hash: String): Algorithm -internal expect fun EcdhKeyDeriveAlgorithm(publicKey: CryptoKey): Algorithm +internal expect fun KeyDeriveAlgorithm(name: String, publicKey: CryptoKey): Algorithm internal expect fun Pbkdf2DeriveAlgorithm(hash: String, iterations: Int, salt: ByteArray): Algorithm @@ -53,3 +53,4 @@ internal expect fun RsaOaepCipherAlgorithm(label: ByteArray?): Algorithm internal expect fun RsaPssSignatureAlgorithm(saltLength: Int): Algorithm internal expect val Algorithm.rsaKeyAlgorithmHashName: String +internal expect val Algorithm.algorithmName: String diff --git a/cryptography-providers/webcrypto/src/jsMain/kotlin/internal/Algorithms.js.kt b/cryptography-providers/webcrypto/src/jsMain/kotlin/internal/Algorithms.js.kt index 413d12d5..8e831be8 100644 --- a/cryptography-providers/webcrypto/src/jsMain/kotlin/internal/Algorithms.js.kt +++ b/cryptography-providers/webcrypto/src/jsMain/kotlin/internal/Algorithms.js.kt @@ -48,8 +48,8 @@ private fun ecKeyAlgorithmNamedCurve(algorithm: Algorithm): String = js("algorit internal actual fun EcdsaSignatureAlgorithm(hash: String): Algorithm = js("{ name: 'ECDSA', hash: hash }").unsafeCast() -internal actual fun EcdhKeyDeriveAlgorithm(publicKey: CryptoKey): Algorithm = - js("{ name: 'ECDH', public: publicKey }").unsafeCast() +internal actual fun KeyDeriveAlgorithm(name: String, publicKey: CryptoKey): Algorithm = + js("{ name: name, public: publicKey }").unsafeCast() internal actual fun Pbkdf2DeriveAlgorithm(hash: String, iterations: Int, salt: ByteArray): Algorithm = js("{ name: 'PBKDF2', hash: hash, iterations: iterations, salt: salt }").unsafeCast() @@ -77,3 +77,8 @@ internal actual val Algorithm.rsaKeyAlgorithmHashName: String get() = rsaKeyAlgo @Suppress("UNUSED_PARAMETER") private fun rsaKeyAlgorithmHashName(algorithm: Algorithm): String = js("algorithm.hash.name").unsafeCast() + +internal actual val Algorithm.algorithmName: String get() = algorithmName(this) + +@Suppress("UNUSED_PARAMETER") +private fun algorithmName(algorithm: Algorithm): String = js("algorithm.name").unsafeCast() diff --git a/cryptography-providers/webcrypto/src/wasmJsMain/kotlin/internal/Algorithms.wasmJs.kt b/cryptography-providers/webcrypto/src/wasmJsMain/kotlin/internal/Algorithms.wasmJs.kt index d1c8c65e..bb7a2f7f 100644 --- a/cryptography-providers/webcrypto/src/wasmJsMain/kotlin/internal/Algorithms.wasmJs.kt +++ b/cryptography-providers/webcrypto/src/wasmJsMain/kotlin/internal/Algorithms.wasmJs.kt @@ -61,8 +61,8 @@ private fun ecKeyAlgorithmNamedCurve(algorithm: Algorithm): String = js("algorit internal actual fun EcdsaSignatureAlgorithm(hash: String): Algorithm = js("({ name: 'ECDSA', hash: hash })") -internal actual fun EcdhKeyDeriveAlgorithm(publicKey: CryptoKey): Algorithm = - js("({ name: 'ECDH', public: publicKey })") +internal actual fun KeyDeriveAlgorithm(name: String, publicKey: CryptoKey): Algorithm = + js("({ name: name, public: publicKey })") internal actual fun Pbkdf2DeriveAlgorithm(hash: String, iterations: Int, salt: ByteArray): Algorithm = jsPbkdf2DeriveAlgorithm(hash, iterations, salt.toInt8Array()) @@ -105,3 +105,8 @@ internal actual val Algorithm.rsaKeyAlgorithmHashName: String get() = rsaKeyAlgo @Suppress("UNUSED_PARAMETER") private fun rsaKeyAlgorithmHashName(algorithm: Algorithm): String = js("algorithm.hash.name") + +internal actual val Algorithm.algorithmName: String get() = algorithmName(this) + +@Suppress("UNUSED_PARAMETER") +private fun algorithmName(algorithm: Algorithm): String = js("algorithm.name") From b100f2133187e684f27c5389644463091f955212 Mon Sep 17 00:00:00 2001 From: Aria Wisp Date: Sat, 6 Sep 2025 20:39:22 -0600 Subject: [PATCH 2/2] docs(webcrypto): add EdDSA/XDH availability note with link; add RFC references --- docs/providers/webcrypto.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/providers/webcrypto.md b/docs/providers/webcrypto.md index 7fc4a11b..b758cbe1 100644 --- a/docs/providers/webcrypto.md +++ b/docs/providers/webcrypto.md @@ -9,6 +9,7 @@ For supported targets and algorithms, please consult [Supported primitives secti * only `suspend` functions are supported, because `WebCrypto` API is async by default * AES.* (browser only): may not support `192 bit` keys * AES.CBC: only `padding=true` is supported +* EdDSA/XDH were added later to WebCrypto and might not be available in all browsers (https://github.com/w3c/webcrypto/pull/362) ## Example @@ -32,3 +33,6 @@ dependencies { [WebCrypto]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API [Supported primitives section]: index.md#supported-primitives + +[RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032 +[RFC 7748]: https://www.rfc-editor.org/rfc/rfc7748