From b6b628dde3b2bc2e37800f315440e507f2ca2605 Mon Sep 17 00:00:00 2001 From: Guillermo Orellana Date: Tue, 15 Oct 2024 15:30:30 +0200 Subject: [PATCH 1/3] allow for half secret keys this requires exposing a method to calculate the public key from the secret key --- gradle.properties | 1 - .../tweetnacl/ed25519/Ed25519Keypair.kt | 21 +++++++++++----- .../avianlabs/solana/tweetnacl/Ed25519Test.kt | 24 +++++++++++++++++++ .../solana/tweetnacl}/SecretBoxTest.kt | 0 .../tweetnacl/ed25519/Ed25519Keypair.kt | 10 ++++++++ .../src/nativeInterop/cinterop/TweetNaCl.def | 3 ++- .../avianlabs/solana/tweetnacl/ByteArray.kt | 19 +++++++++++++++ .../avianlabs/solana/tweetnacl/TweetNaCl.kt | 10 -------- .../tweetnacl/ed25519/Ed25519Keypair.kt | 17 +++++++++++++ .../vendor/tweetnacl/tweetnacl.c | 16 +++++++++++-- 10 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 tweetnacl-multiplatform/src/commonTest/kotlin/net/avianlabs/solana/tweetnacl/Ed25519Test.kt rename tweetnacl-multiplatform/src/commonTest/kotlin/{ => net/avianlabs/solana/tweetnacl}/SecretBoxTest.kt (100%) create mode 100644 tweetnacl-multiplatform/src/jvmMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt create mode 100644 tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ByteArray.kt create mode 100644 tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt diff --git a/gradle.properties b/gradle.properties index fa16562..01757eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,6 @@ version=0.2.1 # Kotlin #kotlin.code.style=official -kotlin.js.compiler=ir kotlin.incremental.multiplatform=true kotlin.mpp.stability.nowarn=true android.useAndroidX=true diff --git a/tweetnacl-multiplatform/src/commonMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt b/tweetnacl-multiplatform/src/commonMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt index b9321cf..4047bfc 100644 --- a/tweetnacl-multiplatform/src/commonMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt +++ b/tweetnacl-multiplatform/src/commonMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt @@ -1,8 +1,6 @@ package net.avianlabs.solana.tweetnacl.ed25519 import net.avianlabs.solana.tweetnacl.TweetNaCl -import net.avianlabs.solana.tweetnacl.TweetNaCl.Signature.Companion.PUBLIC_KEY_BYTES -import net.avianlabs.solana.tweetnacl.TweetNaCl.Signature.Companion.SECRET_KEY_BYTES import net.avianlabs.solana.tweetnacl.vendor.decodeBase58 public data class Ed25519Keypair( @@ -31,13 +29,24 @@ public data class Ed25519Keypair( TweetNaCl.Signature.sign(message = message, secretKey = secretKey) public companion object { - public fun fromSecretKeyBytes(bytes: ByteArray): Ed25519Keypair { - require(bytes.size == SECRET_KEY_BYTES) { "Invalid key length: ${bytes.size}" } - val publicKey = PublicKey(bytes.sliceArray(PUBLIC_KEY_BYTES until SECRET_KEY_BYTES)) - return Ed25519Keypair(publicKey, bytes.copyOf()) + public fun fromSecretKeyBytes(bytes: ByteArray): Ed25519Keypair = when (bytes.size) { + // [secretKey(32)] + 32 -> { + val publicKeyBytes = generatePublicKeyBytes(bytes) + Ed25519Keypair(PublicKey(publicKeyBytes), bytes + publicKeyBytes) + } + // [secretKey(32)|publicKey(32)] + 64 -> { + val publicKey = PublicKey(bytes.sliceArray(32 until 64)) + Ed25519Keypair(publicKey, bytes.copyOf()) + } + + else -> error("Invalid key length: ${bytes.size}") } public fun fromBase58(base58: String): Ed25519Keypair = fromSecretKeyBytes(base58.decodeBase58()) } } + +internal expect fun generatePublicKeyBytes(secretKey: ByteArray): ByteArray diff --git a/tweetnacl-multiplatform/src/commonTest/kotlin/net/avianlabs/solana/tweetnacl/Ed25519Test.kt b/tweetnacl-multiplatform/src/commonTest/kotlin/net/avianlabs/solana/tweetnacl/Ed25519Test.kt new file mode 100644 index 0000000..162f8dd --- /dev/null +++ b/tweetnacl-multiplatform/src/commonTest/kotlin/net/avianlabs/solana/tweetnacl/Ed25519Test.kt @@ -0,0 +1,24 @@ +package net.avianlabs.solana.tweetnacl + +import net.avianlabs.solana.tweetnacl.ed25519.Ed25519Keypair +import net.avianlabs.solana.tweetnacl.vendor.decodeBase58 +import net.avianlabs.solana.tweetnacl.vendor.encodeToBase58String +import kotlin.test.Test +import kotlin.test.assertEquals + +class Ed25519Test { + private val secretKey = + "ftqmzZS6Va5xuyCdks47WZd1D8FpXZaPGqL3JE39814pReiEuTAvJpr8PxkUxm9wHKHTsfN8TGk44hhoEdQDYrD".decodeBase58() + + @Test + fun test_chopping_and_restoring() { + val chopped = secretKey.take(32).toByteArray() + + val restored = Ed25519Keypair.fromSecretKeyBytes(chopped).secretKey + + assertEquals( + secretKey.encodeToBase58String(), + restored.encodeToBase58String(), + ) + } +} diff --git a/tweetnacl-multiplatform/src/commonTest/kotlin/SecretBoxTest.kt b/tweetnacl-multiplatform/src/commonTest/kotlin/net/avianlabs/solana/tweetnacl/SecretBoxTest.kt similarity index 100% rename from tweetnacl-multiplatform/src/commonTest/kotlin/SecretBoxTest.kt rename to tweetnacl-multiplatform/src/commonTest/kotlin/net/avianlabs/solana/tweetnacl/SecretBoxTest.kt diff --git a/tweetnacl-multiplatform/src/jvmMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt b/tweetnacl-multiplatform/src/jvmMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt new file mode 100644 index 0000000..8243d43 --- /dev/null +++ b/tweetnacl-multiplatform/src/jvmMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt @@ -0,0 +1,10 @@ +package net.avianlabs.solana.tweetnacl.ed25519 + +import org.bouncycastle.math.ec.rfc8032.Ed25519 + +internal actual fun generatePublicKeyBytes(secretKey: ByteArray): ByteArray { + val bytes = ByteArray(32) + val pp = Ed25519.generatePublicKey(secretKey, 0) + Ed25519.encodePublicPoint(pp, bytes, 0) + return bytes +} diff --git a/tweetnacl-multiplatform/src/nativeInterop/cinterop/TweetNaCl.def b/tweetnacl-multiplatform/src/nativeInterop/cinterop/TweetNaCl.def index 72497b9..a8c4e1d 100644 --- a/tweetnacl-multiplatform/src/nativeInterop/cinterop/TweetNaCl.def +++ b/tweetnacl-multiplatform/src/nativeInterop/cinterop/TweetNaCl.def @@ -1,2 +1,3 @@ --- -extern int is_on_curve(const unsigned char *); \ No newline at end of file +extern int is_on_curve(const unsigned char *); +extern int public_key_from_secret(unsigned char *, unsigned char *); diff --git a/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ByteArray.kt b/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ByteArray.kt new file mode 100644 index 0000000..dab5326 --- /dev/null +++ b/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ByteArray.kt @@ -0,0 +1,19 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package net.avianlabs.solana.tweetnacl + +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.UByteVar +import kotlinx.cinterop.get + +internal fun CPointer.toByteArray(length: Int): ByteArray { + val nativeBytes = this + val bytes = ByteArray(length) + var index = 0 + while (index < length) { + bytes[index] = nativeBytes[index].toByte() + ++index + } + return bytes +} diff --git a/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/TweetNaCl.kt b/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/TweetNaCl.kt index f2ab971..49919b3 100644 --- a/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/TweetNaCl.kt +++ b/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/TweetNaCl.kt @@ -121,13 +121,3 @@ internal actual fun secretBoxInternal(secretKey: ByteArray): TweetNaCl.SecretBox }.drop(crypto_secretbox_xsalsa20poly1305_tweet_ZEROBYTES).toByteArray() } -private fun CPointer.toByteArray(length: Int): ByteArray { - val nativeBytes = this - val bytes = ByteArray(length) - var index = 0 - while (index < length) { - bytes[index] = nativeBytes[index].toByte() - ++index - } - return bytes -} diff --git a/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt b/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt new file mode 100644 index 0000000..7c47a81 --- /dev/null +++ b/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt @@ -0,0 +1,17 @@ +@file:OptIn(ExperimentalForeignApi::class) + +package net.avianlabs.solana.tweetnacl.ed25519 + +import kotlinx.cinterop.* +import net.avianlabs.solana.tweetnacl.crypto_sign_ed25519_tweet_PUBLICKEYBYTES +import net.avianlabs.solana.tweetnacl.public_key_from_secret +import net.avianlabs.solana.tweetnacl.toByteArray + +@OptIn(ExperimentalForeignApi::class) +internal actual fun generatePublicKeyBytes(secretKey: ByteArray): ByteArray = memScoped { + val pkPointer = allocArray(crypto_sign_ed25519_tweet_PUBLICKEYBYTES) + + public_key_from_secret(secretKey.asUByteArray().toCValues(), pkPointer) + + pkPointer.toByteArray(crypto_sign_ed25519_tweet_PUBLICKEYBYTES) +} diff --git a/tweetnacl-multiplatform/vendor/tweetnacl/tweetnacl.c b/tweetnacl-multiplatform/vendor/tweetnacl/tweetnacl.c index 4aa6992..885d43a 100644 --- a/tweetnacl-multiplatform/vendor/tweetnacl/tweetnacl.c +++ b/tweetnacl-multiplatform/vendor/tweetnacl/tweetnacl.c @@ -807,9 +807,21 @@ int crypto_sign_open(u8 *m,u64 *mlen,const u8 *sm,u64 n,const u8 *pk) // curve additions -int is_on_curve(const u8 p[32]) { +int is_on_curve(const u8 p[32]) +{ gf q[4]; - return unpackneg(q,p); + + return unpackneg(q, p); +} + +int public_key_from_secret(u8 *sk, u8 *pk) +{ + gf a[4]; + + scalarbase(a, sk); + pack(pk, a); + + return 0; } // END curve additions From 3c6fa5c39d8294e6c21dca379fbf64e5fb161286 Mon Sep 17 00:00:00 2001 From: Guillermo Orellana Date: Tue, 15 Oct 2024 16:26:02 +0200 Subject: [PATCH 2/3] test hex --- .../kotlin/net/avianlabs/solana/tweetnacl/Ed25519Test.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tweetnacl-multiplatform/src/commonTest/kotlin/net/avianlabs/solana/tweetnacl/Ed25519Test.kt b/tweetnacl-multiplatform/src/commonTest/kotlin/net/avianlabs/solana/tweetnacl/Ed25519Test.kt index 162f8dd..654248b 100644 --- a/tweetnacl-multiplatform/src/commonTest/kotlin/net/avianlabs/solana/tweetnacl/Ed25519Test.kt +++ b/tweetnacl-multiplatform/src/commonTest/kotlin/net/avianlabs/solana/tweetnacl/Ed25519Test.kt @@ -2,7 +2,6 @@ package net.avianlabs.solana.tweetnacl import net.avianlabs.solana.tweetnacl.ed25519.Ed25519Keypair import net.avianlabs.solana.tweetnacl.vendor.decodeBase58 -import net.avianlabs.solana.tweetnacl.vendor.encodeToBase58String import kotlin.test.Test import kotlin.test.assertEquals @@ -16,9 +15,10 @@ class Ed25519Test { val restored = Ed25519Keypair.fromSecretKeyBytes(chopped).secretKey + @OptIn(ExperimentalStdlibApi::class) assertEquals( - secretKey.encodeToBase58String(), - restored.encodeToBase58String(), + secretKey.toHexString(), + restored.toHexString(), ) } } From 76355f4d91527700f130df5a2e80d0abbd652d92 Mon Sep 17 00:00:00 2001 From: Guillermo Orellana Date: Tue, 15 Oct 2024 17:15:10 +0200 Subject: [PATCH 3/3] simpler approach --- .../solana/tweetnacl/ed25519/Ed25519Keypair.kt | 7 +------ .../solana/tweetnacl/ed25519/Ed25519Keypair.kt | 10 ---------- .../src/nativeInterop/cinterop/TweetNaCl.def | 1 - .../solana/tweetnacl/ed25519/Ed25519Keypair.kt | 17 ----------------- .../vendor/tweetnacl/tweetnacl.c | 10 ---------- 5 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 tweetnacl-multiplatform/src/jvmMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt delete mode 100644 tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt diff --git a/tweetnacl-multiplatform/src/commonMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt b/tweetnacl-multiplatform/src/commonMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt index 4047bfc..1d1a5f7 100644 --- a/tweetnacl-multiplatform/src/commonMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt +++ b/tweetnacl-multiplatform/src/commonMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt @@ -31,10 +31,7 @@ public data class Ed25519Keypair( public companion object { public fun fromSecretKeyBytes(bytes: ByteArray): Ed25519Keypair = when (bytes.size) { // [secretKey(32)] - 32 -> { - val publicKeyBytes = generatePublicKeyBytes(bytes) - Ed25519Keypair(PublicKey(publicKeyBytes), bytes + publicKeyBytes) - } + 32 -> TweetNaCl.Signature.generateKey(bytes + ByteArray(32)) // [secretKey(32)|publicKey(32)] 64 -> { val publicKey = PublicKey(bytes.sliceArray(32 until 64)) @@ -48,5 +45,3 @@ public data class Ed25519Keypair( fromSecretKeyBytes(base58.decodeBase58()) } } - -internal expect fun generatePublicKeyBytes(secretKey: ByteArray): ByteArray diff --git a/tweetnacl-multiplatform/src/jvmMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt b/tweetnacl-multiplatform/src/jvmMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt deleted file mode 100644 index 8243d43..0000000 --- a/tweetnacl-multiplatform/src/jvmMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.avianlabs.solana.tweetnacl.ed25519 - -import org.bouncycastle.math.ec.rfc8032.Ed25519 - -internal actual fun generatePublicKeyBytes(secretKey: ByteArray): ByteArray { - val bytes = ByteArray(32) - val pp = Ed25519.generatePublicKey(secretKey, 0) - Ed25519.encodePublicPoint(pp, bytes, 0) - return bytes -} diff --git a/tweetnacl-multiplatform/src/nativeInterop/cinterop/TweetNaCl.def b/tweetnacl-multiplatform/src/nativeInterop/cinterop/TweetNaCl.def index a8c4e1d..619b0e7 100644 --- a/tweetnacl-multiplatform/src/nativeInterop/cinterop/TweetNaCl.def +++ b/tweetnacl-multiplatform/src/nativeInterop/cinterop/TweetNaCl.def @@ -1,3 +1,2 @@ --- extern int is_on_curve(const unsigned char *); -extern int public_key_from_secret(unsigned char *, unsigned char *); diff --git a/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt b/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt deleted file mode 100644 index 7c47a81..0000000 --- a/tweetnacl-multiplatform/src/nativeMain/kotlin/net/avianlabs/solana/tweetnacl/ed25519/Ed25519Keypair.kt +++ /dev/null @@ -1,17 +0,0 @@ -@file:OptIn(ExperimentalForeignApi::class) - -package net.avianlabs.solana.tweetnacl.ed25519 - -import kotlinx.cinterop.* -import net.avianlabs.solana.tweetnacl.crypto_sign_ed25519_tweet_PUBLICKEYBYTES -import net.avianlabs.solana.tweetnacl.public_key_from_secret -import net.avianlabs.solana.tweetnacl.toByteArray - -@OptIn(ExperimentalForeignApi::class) -internal actual fun generatePublicKeyBytes(secretKey: ByteArray): ByteArray = memScoped { - val pkPointer = allocArray(crypto_sign_ed25519_tweet_PUBLICKEYBYTES) - - public_key_from_secret(secretKey.asUByteArray().toCValues(), pkPointer) - - pkPointer.toByteArray(crypto_sign_ed25519_tweet_PUBLICKEYBYTES) -} diff --git a/tweetnacl-multiplatform/vendor/tweetnacl/tweetnacl.c b/tweetnacl-multiplatform/vendor/tweetnacl/tweetnacl.c index 885d43a..137ccf5 100644 --- a/tweetnacl-multiplatform/vendor/tweetnacl/tweetnacl.c +++ b/tweetnacl-multiplatform/vendor/tweetnacl/tweetnacl.c @@ -814,14 +814,4 @@ int is_on_curve(const u8 p[32]) return unpackneg(q, p); } -int public_key_from_secret(u8 *sk, u8 *pk) -{ - gf a[4]; - - scalarbase(a, sk); - pack(pk, a); - - return 0; -} - // END curve additions