Skip to content
Draft
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ compose-multiplatform = "1.7.3"
junit = "4.13.2"
kotlin = "2.1.20"
kotlinx-coroutines = "1.10.2"
kotlinx-serialization = "1.7.3"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
Expand All @@ -30,6 +31,7 @@ androidx-activity-compose = { module = "androidx.activity:activity-compose", ver
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
Expand All @@ -38,4 +40,5 @@ composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-mu
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
dokka = { id = "org.jetbrains.dokka", version = "2.0.0" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
publishing = { id = "com.vanniktech.maven.publish", version = "0.31.0" }
3 changes: 3 additions & 0 deletions modules/asymmetric/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ kotlin {
sourceSets {
commonMain.dependencies {
}
nativeMain.dependencies {
implementation(projects.modules.hashing)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.wannaverse.crypto.asymmetric.ecdsa

import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.Signature
import java.security.interfaces.ECPrivateKey
import java.security.interfaces.ECPublicKey
import java.security.spec.ECGenParameterSpec
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec

actual object ECDSA {
actual fun generateKeyPair(curve: ECDSACurve): ECDSAKeyPair {
val keyGen = KeyPairGenerator.getInstance("EC")
keyGen.initialize(ECGenParameterSpec(curve.curveName))
val keyPair = keyGen.generateKeyPair()
return ECDSAKeyPair(
publicKey = keyPair.public.encoded,
privateKey = keyPair.private.encoded,
curve = curve
)
}

actual fun sign(privateKey: ByteArray, data: ByteArray, curve: ECDSACurve): ByteArray {
val keyFactory = KeyFactory.getInstance("EC")
val privKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(privateKey)) as ECPrivateKey

val sig = Signature.getInstance(signatureAlgorithm(curve))
sig.initSign(privKey)
sig.update(data)
return sig.sign()
}

actual fun verify(publicKey: ByteArray, data: ByteArray, signature: ByteArray, curve: ECDSACurve): Boolean {
val keyFactory = KeyFactory.getInstance("EC")
val pubKey = keyFactory.generatePublic(X509EncodedKeySpec(publicKey)) as ECPublicKey

val sig = Signature.getInstance(signatureAlgorithm(curve))
sig.initVerify(pubKey)
sig.update(data)
return sig.verify(signature)
}

private fun signatureAlgorithm(curve: ECDSACurve): String = when (curve) {
ECDSACurve.P256 -> "SHA256withECDSA"
ECDSACurve.P384 -> "SHA384withECDSA"
ECDSACurve.P521 -> "SHA512withECDSA"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.wannaverse.crypto.asymmetric.ecdsa

/**
* Provides ECDSA (Elliptic Curve Digital Signature Algorithm) operations.
*
* Supports P-256, P-384, and P-521 curves. Signatures are produced in DER encoding.
*/
expect object ECDSA {
/**
* Generates an ECDSA key pair for the specified curve.
*
* @param curve The elliptic curve to use. Defaults to [ECDSACurve.P256].
* @return An [ECDSAKeyPair] containing the generated public and private keys.
*/
fun generateKeyPair(curve: ECDSACurve = ECDSACurve.P256): ECDSAKeyPair

/**
* Signs the given data using the provided ECDSA private key.
*
* The hash algorithm is determined by the curve: SHA-256 for P-256, SHA-384 for P-384, SHA-512 for P-521.
*
* @param privateKey The private key in PKCS#8 encoded format.
* @param data The data to sign as a byte array.
* @param curve The elliptic curve used. Defaults to [ECDSACurve.P256].
* @return The DER-encoded signature as a byte array.
*/
fun sign(privateKey: ByteArray, data: ByteArray, curve: ECDSACurve = ECDSACurve.P256): ByteArray

/**
* Verifies a DER-encoded signature using the provided ECDSA public key.
*
* @param publicKey The public key in X.509 encoded format.
* @param data The original data that was signed.
* @param signature The DER-encoded signature to verify.
* @param curve The elliptic curve used. Defaults to [ECDSACurve.P256].
* @return `true` if the signature is valid, `false` otherwise.
*/
fun verify(publicKey: ByteArray, data: ByteArray, signature: ByteArray, curve: ECDSACurve = ECDSACurve.P256): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.wannaverse.crypto.asymmetric.ecdsa

/**
* Enum representing supported ECDSA curves.
*
* @property curveName The standard curve name used by cryptographic providers.
* @property keySize The key size in bits.
* @property componentLength The byte length of each R and S signature component.
*/
enum class ECDSACurve(val curveName: String, val keySize: Int, val componentLength: Int) {
P256("secp256r1", 256, 32),
P384("secp384r1", 384, 48),
P521("secp521r1", 521, 66)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.wannaverse.crypto.asymmetric.ecdsa

/**
* Represents an ECDSA key pair.
*
* @property publicKey The public key in X.509 encoded format.
* @property privateKey The private key in PKCS#8 encoded format.
* @property curve The elliptic curve used for this key pair.
*/
data class ECDSAKeyPair(
val publicKey: ByteArray,
val privateKey: ByteArray,
val curve: ECDSACurve
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is ECDSAKeyPair) return false
return publicKey.contentEquals(other.publicKey) &&
privateKey.contentEquals(other.privateKey) &&
curve == other.curve
}

override fun hashCode(): Int {
var result = publicKey.contentHashCode()
result = 31 * result + privateKey.contentHashCode()
result = 31 * result + curve.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,29 @@ enum class RSASignaturePadding(val signatureString: String) {
*/
PKCS1_SHA256("SHA256withRSA"),

/**
* PKCS#1 v1.5 padding with SHA-384.
*/
PKCS1_SHA384("SHA384withRSA"),

/**
* PKCS#1 v1.5 padding with SHA-512.
*/
PKCS1_SHA512("SHA512withRSA"),

/**
* RSASSA-PSS (Probabilistic Signature Scheme) with SHA-256.
* A modern signature scheme that provides better security and is recommended for new applications.
*/
PSS_SHA256("SHA256withRSA/PSS")
PSS_SHA256("SHA256withRSA/PSS"),

/**
* RSASSA-PSS with SHA-384.
*/
PSS_SHA384("SHA384withRSA/PSS"),

/**
* RSASSA-PSS with SHA-512.
*/
PSS_SHA512("SHA512withRSA/PSS")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)

package com.wannaverse.crypto.asymmetric.ecdsa

import com.wannaverse.crypto.asymmetric.util.DerUtils
import com.wannaverse.crypto.asymmetric.util.SecKeyHelper
import platform.CoreFoundation.CFRelease
import platform.Security.*

actual object ECDSA {

actual fun generateKeyPair(curve: ECDSACurve): ECDSAKeyPair {
val (privRef, pubRef) = SecKeyHelper.generateKeyPair(
kSecAttrKeyTypeECSECPrimeRandom, curve.keySize
)

// Export raw keys from SecKey
// Public: uncompressed point (04||X||Y)
// Private: (04||X||Y||D) concatenation
val rawPublic = SecKeyHelper.exportKey(pubRef)
val rawPrivate = SecKeyHelper.exportKey(privRef)

CFRelease(privRef)
CFRelease(pubRef)

val curveOid = DerUtils.curveOidForName(curve.curveName)
val algId = DerUtils.ecAlgorithmIdentifier(curveOid)

// Wrap public key in X.509 format
val x509Public = DerUtils.wrapInX509(rawPublic, algId)

// Extract private scalar and build SEC1, then wrap in PKCS8
val pointSize = rawPublic.size // 04||X||Y
val privateScalar = rawPrivate.copyOfRange(pointSize, rawPrivate.size)
val sec1 = DerUtils.buildSec1EcPrivateKey(privateScalar, curveOid, rawPublic)
val pkcs8Private = DerUtils.wrapInPkcs8(sec1, algId)

return ECDSAKeyPair(
publicKey = x509Public,
privateKey = pkcs8Private,
curve = curve
)
}

actual fun sign(privateKey: ByteArray, data: ByteArray, curve: ECDSACurve): ByteArray {
val rawKey = importPrivateKey(privateKey, curve)
val keyRef = SecKeyHelper.createSecKey(
rawKey, kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClassPrivate, curve.keySize
)

val result = SecKeyHelper.sign(keyRef, signatureAlgorithm(curve), data)
CFRelease(keyRef)
return result // DER-encoded, matching the expect API contract
}

actual fun verify(
publicKey: ByteArray,
data: ByteArray,
signature: ByteArray,
curve: ECDSACurve
): Boolean {
// Extract raw point from X.509
val rawPoint = DerUtils.stripX509Header(publicKey)
val keyRef = SecKeyHelper.createSecKey(
rawPoint, kSecAttrKeyTypeECSECPrimeRandom, kSecAttrKeyClassPublic, curve.keySize
)

val result = SecKeyHelper.verify(keyRef, signatureAlgorithm(curve), data, signature)
CFRelease(keyRef)
return result
}

/**
* Converts a PKCS8-encoded EC private key to the raw format expected by iOS SecKey.
* iOS format: (04||X||Y||D) — the uncompressed public point concatenated with the private scalar.
*/
private fun importPrivateKey(pkcs8Key: ByteArray, curve: ECDSACurve): ByteArray {
// PKCS8 → SEC1
val sec1 = DerUtils.stripPkcs8Header(pkcs8Key)

// SEC1 → raw scalar + public point
val (privateScalar, publicPoint) = DerUtils.parseSec1EcPrivateKey(sec1)

if (publicPoint != null) {
// iOS format: public point || private scalar
// Pad the scalar to the expected component length
val paddedScalar = padToLength(privateScalar, curve.componentLength)
return publicPoint + paddedScalar
}

throw RuntimeException("EC private key does not contain public point; cannot import to iOS")
}

private fun signatureAlgorithm(curve: ECDSACurve): SecKeyAlgorithm? = when (curve) {
ECDSACurve.P256 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA256
ECDSACurve.P384 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA384
ECDSACurve.P521 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA512
}

private fun padToLength(data: ByteArray, length: Int): ByteArray {
return if (data.size >= length) {
data.copyOfRange(data.size - length, data.size)
} else {
ByteArray(length - data.size) + data
}
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,52 @@
@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)

package com.wannaverse.crypto.asymmetric.ed25519

import com.wannaverse.crypto.asymmetric.util.DerUtils
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import platform.Security.SecRandomCopyBytes
import platform.Security.kSecRandomDefault

actual object ED25519 {

actual fun generateKeyPair(): ED25519KeyPair {
TODO("Not yet implemented")
// Generate a random 32-byte seed
val seed = secureRandomBytes(32)

// Derive public key from seed using pure Kotlin Ed25519
val rawPublicKey = Ed25519Internals.generatePublicKey(seed)

// Wrap in standard formats for cross-platform compatibility
val x509Public = DerUtils.wrapEd25519PublicKey(rawPublicKey)
val pkcs8Private = DerUtils.wrapEd25519PrivateKey(seed)

return ED25519KeyPair(
publicKey = x509Public,
privateKey = pkcs8Private
)
}

actual fun sign(privateKey: ByteArray, data: ByteArray): ByteArray {
TODO("Not yet implemented")
// Extract the raw 32-byte seed from PKCS8
val seed = DerUtils.stripEd25519PrivateKey(privateKey)
return Ed25519Internals.sign(seed, data)
}

actual fun verify(
publicKey: ByteArray,
data: ByteArray,
signatureBytes: ByteArray
): Boolean {
TODO("Not yet implemented")
actual fun verify(publicKey: ByteArray, data: ByteArray, signatureBytes: ByteArray): Boolean {
// Extract raw 32-byte public key from X.509
val rawPublicKey = DerUtils.stripX509Header(publicKey)
return Ed25519Internals.verify(rawPublicKey, data, signatureBytes)
}

}
private fun secureRandomBytes(length: Int): ByteArray {
val result = ByteArray(length)
result.usePinned {
val status = SecRandomCopyBytes(kSecRandomDefault, length.toULong(), it.addressOf(0))
if (status != 0) {
throw RuntimeException("Failed to generate secure random bytes: $status")
}
}
return result
}
}
Loading