From 62449c772c1fa32926924e0063f0acd7178d87d7 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 7 Aug 2026 15:43:32 -0400 Subject: [PATCH 1/7] =?UTF-8?q?feat(kmp):=20ed25519-kmp=20module=20?= =?UTF-8?q?=E2=80=94=20cinterop=20iOS=20actual=20+=20JNI=20Android=20actua?= =?UTF-8?q?l=20(B1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New KMP module :libs:encryption:ed25519-kmp exposing Ed25519Kmp (createKeyPair, sign, verify, onCurve) to commonMain via expect/actual - iosMain actual: Kotlin/Native cinterop to vendored orlp/ed25519 C sources; per-target static archive compiled via Gradle Exec task - androidMain actual: delegates to existing com.getcode.ed25519.Ed25519 JNI - commonTest: RFC 8032 vector gate reads ed25519.json on Android host + iOS - Wires into :kmp:shared-core (api + export in SharedCore XCFramework) - Existing :libs:encryption:ed25519 (JNI + CMake) left untouched --- kmp/shared-core/build.gradle.kts | 2 + libs/encryption/ed25519-kmp/build.gradle.kts | 121 ++++++++++++++++++ .../ed25519-kmp/cinterop/ed25519.def | 2 + .../ed25519kmp/TestResources.android.kt | 6 + .../getcode/ed25519kmp/Ed25519Kmp.android.kt | 39 ++++++ .../ed25519kmp/TestResources.android.kt | 7 + .../com/getcode/ed25519kmp/Ed25519Kmp.kt | 40 ++++++ .../getcode/ed25519kmp/Ed25519VectorTest.kt | 105 +++++++++++++++ .../com/getcode/ed25519kmp/TestResources.kt | 4 + .../src/commonTest/resources/ed25519.json | 49 +++++++ .../com/getcode/ed25519kmp/Ed25519Kmp.ios.kt | 75 +++++++++++ .../getcode/ed25519kmp/TestResources.ios.kt | 17 +++ settings.gradle.kts | 2 + 13 files changed, 469 insertions(+) create mode 100644 libs/encryption/ed25519-kmp/build.gradle.kts create mode 100644 libs/encryption/ed25519-kmp/cinterop/ed25519.def create mode 100644 libs/encryption/ed25519-kmp/src/androidHostTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt create mode 100644 libs/encryption/ed25519-kmp/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt create mode 100644 libs/encryption/ed25519-kmp/src/androidTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt create mode 100644 libs/encryption/ed25519-kmp/src/commonMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.kt create mode 100644 libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/Ed25519VectorTest.kt create mode 100644 libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/TestResources.kt create mode 100644 libs/encryption/ed25519-kmp/src/commonTest/resources/ed25519.json create mode 100644 libs/encryption/ed25519-kmp/src/iosMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.ios.kt create mode 100644 libs/encryption/ed25519-kmp/src/iosTest/kotlin/com/getcode/ed25519kmp/TestResources.ios.kt diff --git a/kmp/shared-core/build.gradle.kts b/kmp/shared-core/build.gradle.kts index 90ab11b4a4..0139999c4d 100644 --- a/kmp/shared-core/build.gradle.kts +++ b/kmp/shared-core/build.gradle.kts @@ -24,6 +24,7 @@ kotlin { export(project(":libs:encryption:sha256")) export(project(":libs:encryption:sha512")) export(project(":libs:encryption:hmac")) + export(project(":libs:encryption:ed25519-kmp")) } } @@ -34,6 +35,7 @@ kotlin { api(project(":libs:encryption:sha256")) api(project(":libs:encryption:sha512")) api(project(":libs:encryption:hmac")) + api(project(":libs:encryption:ed25519-kmp")) } } } diff --git a/libs/encryption/ed25519-kmp/build.gradle.kts b/libs/encryption/ed25519-kmp/build.gradle.kts new file mode 100644 index 0000000000..39a35611d0 --- /dev/null +++ b/libs/encryption/ed25519-kmp/build.gradle.kts @@ -0,0 +1,121 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + kotlin("multiplatform") + id("com.android.kotlin.multiplatform.library") +} + +// ── C source paths ──────────────────────────────────────────────────────────── +// The ed25519 C sources live in the existing ed25519 JNI module. +val ed25519SrcDir = rootProject.file("libs/encryption/ed25519/libs/ed25519/src") +val ed25519CSources = fileTree(ed25519SrcDir) { + include("*.c") + // key_exchange.c is omitted — not part of our public API surface. + // seed.c is omitted — uses platform entropy; KMP API takes a caller-supplied seed. + exclude("key_exchange.c", "seed.c") +} + +// ── Static-library compilation per Apple target ─────────────────────────────── +// +// Kotlin/Native cinterop only generates Kotlin bindings from the header; it does +// not compile C sources for Apple targets. We produce a static archive +// (libored25519.a) for each target and tell the linker about it. +// +// Output: build/cinterop//libored25519.a + +data class AppleTarget(val kotlinName: String, val sdk: String, val arch: String) + +val appleTargetDefs = listOf( + AppleTarget("iosArm64", "iphoneos", "arm64"), + AppleTarget("iosSimulatorArm64", "iphonesimulator", "arm64"), + AppleTarget("iosX64", "iphonesimulator", "x86_64"), +) + +appleTargetDefs.forEach { target -> + val outDir = layout.buildDirectory.dir("cinterop/${target.kotlinName}") + tasks.register("compileEd25519C_${target.kotlinName}", Exec::class) { + group = "cinterop" + description = "Compile ed25519 C sources into a static archive for ${target.kotlinName}" + + inputs.files(ed25519CSources) + outputs.dir(outDir) + + doFirst { + outDir.get().asFile.mkdirs() + } + + // Compile each .c → .o then archive all .o into libored25519.a in one shell invocation. + commandLine("sh", "-c", buildString { + val compileLines = ed25519CSources.files.joinToString(" && ") { src -> + val obj = outDir.get().file(src.nameWithoutExtension + ".o").asFile.absolutePath + "xcrun -sdk ${target.sdk} clang -arch ${target.arch} -O2" + + " -c \"${src.absolutePath}\"" + + " -I\"${ed25519SrcDir.absolutePath}\"" + + " -o \"$obj\"" + } + val objPaths = ed25519CSources.files.joinToString(" ") { src -> + "\"${outDir.get().file(src.nameWithoutExtension + ".o").asFile.absolutePath}\"" + } + val libPath = outDir.get().file("libored25519.a").asFile.absolutePath + append(compileLines) + append(" && ar rcs \"$libPath\" $objPaths") + }) + } +} + +kotlin { + android { + namespace = "com.getcode.encryption.ed25519kmp" + compileSdk = 37 + minSdk = 29 + withHostTest {} + } + + iosArm64() + iosSimulatorArm64() + iosX64() + + // ── Cinterop + linker wiring for each Apple target ──────────────────────── + targets.withType().configureEach { + val targetDef = appleTargetDefs.first { it.kotlinName == name } + val libDir = layout.buildDirectory.dir("cinterop/$name") + val compileTaskName = "compileEd25519C_$name" + val cinteropTaskName = "cinteropEd25519${name.replaceFirstChar { it.uppercaseChar() }}" + + compilations["main"].cinterops.create("ed25519") { + defFile = file("cinterop/ed25519.def") + includeDirs(ed25519SrcDir) + } + + // Both the cinterop binding task and the Kotlin compile task need the + // static archive to exist before linking. + tasks.matching { it.name == cinteropTaskName }.configureEach { + dependsOn(compileTaskName) + } + tasks.matching { it.name == "compileKotlin${name.replaceFirstChar { it.uppercaseChar() }}" }.configureEach { + dependsOn(compileTaskName) + } + + binaries.configureEach { + linkerOpts("-L${libDir.get().asFile.absolutePath}", "-lored25519") + } + } + + sourceSets { + commonMain { + // Ed25519Kmp expect object + KeyPair — no external deps. + } + androidMain { + dependencies { + // Delegate to the existing JNI module — no NDK recompilation. + implementation(project(":libs:encryption:ed25519")) + } + } + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.serialization.json) + } + } + } +} diff --git a/libs/encryption/ed25519-kmp/cinterop/ed25519.def b/libs/encryption/ed25519-kmp/cinterop/ed25519.def new file mode 100644 index 0000000000..ab286c0280 --- /dev/null +++ b/libs/encryption/ed25519-kmp/cinterop/ed25519.def @@ -0,0 +1,2 @@ +headers = ed25519.h +compilerOpts = -I../../libs/encryption/ed25519/libs/ed25519/src diff --git a/libs/encryption/ed25519-kmp/src/androidHostTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt b/libs/encryption/ed25519-kmp/src/androidHostTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt new file mode 100644 index 0000000000..7f675d002b --- /dev/null +++ b/libs/encryption/ed25519-kmp/src/androidHostTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt @@ -0,0 +1,6 @@ +package com.getcode.ed25519kmp + +actual fun readTestResource(name: String): String = + checkNotNull(Thread.currentThread().contextClassLoader?.getResourceAsStream(name)) { + "Resource '$name' not found on classpath" + }.bufferedReader().use { it.readText() } diff --git a/libs/encryption/ed25519-kmp/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt b/libs/encryption/ed25519-kmp/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt new file mode 100644 index 0000000000..4a06d0c253 --- /dev/null +++ b/libs/encryption/ed25519-kmp/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt @@ -0,0 +1,39 @@ +package com.getcode.ed25519kmp + +import android.util.Base64 +import com.getcode.ed25519.Ed25519 as JniEd25519 + +/** + * Android actual: delegates to the existing JNI [com.getcode.ed25519.Ed25519]. + * + * The JNI layer encodes/decodes through base64 internally; we handle that + * translation here so [commonMain] consumers always work with raw [ByteArray]. + */ +actual object Ed25519Kmp { + + actual fun createKeyPair(seed: ByteArray): KeyPair { + // Ed25519.createKeyPair expects a base64-encoded seed (Base64.DEFAULT). + val seedB64 = Base64.encodeToString(seed, Base64.DEFAULT) + val jniPair = JniEd25519.createKeyPair(seedB64) + // JniPair.publicKey / privateKey are base64 strings (Base64.DEFAULT). + val publicKey = Base64.decode(jniPair.publicKey, Base64.DEFAULT) + val privateKey = Base64.decode(jniPair.privateKey, Base64.DEFAULT) + return KeyPair(publicKey = publicKey, privateKey = privateKey) + } + + actual fun sign( + message: ByteArray, + publicKey: ByteArray, + privateKey: ByteArray, + ): ByteArray = JniEd25519.Signature(message, privateKey, publicKey) + ?: error("Ed25519.Signature returned null") + + actual fun verify( + signature: ByteArray, + message: ByteArray, + publicKey: ByteArray, + ): Boolean = JniEd25519.Verify(signature, message, publicKey) + + actual fun onCurve(publicKey: ByteArray): Boolean = + JniEd25519.OnCurve(publicKey) +} diff --git a/libs/encryption/ed25519-kmp/src/androidTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt b/libs/encryption/ed25519-kmp/src/androidTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt new file mode 100644 index 0000000000..de9a6d0214 --- /dev/null +++ b/libs/encryption/ed25519-kmp/src/androidTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt @@ -0,0 +1,7 @@ +package com.getcode.ed25519kmp + +import androidx.test.platform.app.InstrumentationRegistry + +actual fun readTestResource(name: String): String = + InstrumentationRegistry.getInstrumentation().context.assets + .open(name).bufferedReader().use { it.readText() } diff --git a/libs/encryption/ed25519-kmp/src/commonMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.kt b/libs/encryption/ed25519-kmp/src/commonMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.kt new file mode 100644 index 0000000000..dc4ee8ae35 --- /dev/null +++ b/libs/encryption/ed25519-kmp/src/commonMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.kt @@ -0,0 +1,40 @@ +package com.getcode.ed25519kmp + +/** + * Cross-platform Ed25519 signatures (RFC 8032 / orlp implementation). + * + * All byte arrays are raw (not base64). Sizes: + * seed = 32 bytes + * publicKey = 32 bytes + * privateKey = 64 bytes (seed || public key, orlp convention) + * signature = 64 bytes + */ +expect object Ed25519Kmp { + + /** Derives a (publicKey, privateKey) pair from a 32-byte seed. */ + fun createKeyPair(seed: ByteArray): KeyPair + + /** + * Signs [message] with [privateKey] (64-byte extended key) + [publicKey]. + * Returns a 64-byte signature. + */ + fun sign(message: ByteArray, publicKey: ByteArray, privateKey: ByteArray): ByteArray + + /** + * Verifies [signature] over [message] using [publicKey]. + * Returns true iff the signature is valid. + */ + fun verify(signature: ByteArray, message: ByteArray, publicKey: ByteArray): Boolean + + /** Returns true iff [publicKey] is a valid point on the Ed25519 curve. */ + fun onCurve(publicKey: ByteArray): Boolean +} + +/** Ed25519 key pair. */ +data class KeyPair(val publicKey: ByteArray, val privateKey: ByteArray) { + override fun equals(other: Any?): Boolean = + other is KeyPair && + publicKey.contentEquals(other.publicKey) && + privateKey.contentEquals(other.privateKey) + override fun hashCode(): Int = 31 * publicKey.contentHashCode() + privateKey.contentHashCode() +} diff --git a/libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/Ed25519VectorTest.kt b/libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/Ed25519VectorTest.kt new file mode 100644 index 0000000000..723706b0df --- /dev/null +++ b/libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/Ed25519VectorTest.kt @@ -0,0 +1,105 @@ +package com.getcode.ed25519kmp + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * GATE: Ed25519 must reproduce RFC 8032 vectors on BOTH Android (JVM host) and iOS (native). + * Fixture: src/commonTest/resources/ed25519.json (canonical source: test-vectors/ed25519.json). + * + * Vector format: + * seed (32-byte hex) + message (hex, may be empty) + * → publicKey (32-byte hex) + signature (64-byte hex) + */ +class Ed25519VectorTest { + + @Test + fun ed25519_createKeyPair_matches_canonical_vectors() { + val vectors = loadVectors() + assertTrue(vectors.isNotEmpty(), "no vectors loaded from ed25519.json") + + for (v in vectors) { + val pair = Ed25519Kmp.createKeyPair(v.seed) + assertEquals( + v.publicKey.toHexString(), pair.publicKey.toHexString(), + "publicKey mismatch for '${v.name}'" + ) + } + } + + @Test + fun ed25519_sign_matches_canonical_vectors() { + val vectors = loadVectors() + for (v in vectors) { + val pair = Ed25519Kmp.createKeyPair(v.seed) + val sig = Ed25519Kmp.sign(v.message, pair.publicKey, pair.privateKey) + assertEquals( + v.signature.toHexString(), sig.toHexString(), + "signature mismatch for '${v.name}'" + ) + } + } + + @Test + fun ed25519_verify_accepts_canonical_signatures() { + val vectors = loadVectors() + for (v in vectors) { + val pair = Ed25519Kmp.createKeyPair(v.seed) + val sig = Ed25519Kmp.sign(v.message, pair.publicKey, pair.privateKey) + assertTrue( + Ed25519Kmp.verify(sig, v.message, pair.publicKey), + "verify returned false for '${v.name}'" + ) + } + } + + @Test + fun ed25519_onCurve_true_for_canonical_public_keys() { + val vectors = loadVectors() + for (v in vectors) { + assertTrue( + Ed25519Kmp.onCurve(v.publicKey), + "onCurve returned false for '${v.name}'" + ) + } + } + + // ── helpers ────────────────────────────────────────────────────────────── + + private data class Vector( + val name: String, + val seed: ByteArray, + val message: ByteArray, + val publicKey: ByteArray, + val signature: ByteArray, + ) + + private fun loadVectors(): List { + val text = readTestResource("ed25519.json") + val root = Json.parseToJsonElement(text).jsonObject + return root["vectors"]!!.jsonArray.map { el -> + val o = el.jsonObject + Vector( + name = o["name"]!!.jsonPrimitive.content, + seed = o["seed"]!!.jsonPrimitive.content.hexToBytes(), + message = o["message"]!!.jsonPrimitive.content.hexToBytes(), + publicKey = o["publicKey"]!!.jsonPrimitive.content.hexToBytes(), + signature = o["signature"]!!.jsonPrimitive.content.hexToBytes(), + ) + } + } + + private fun String.hexToBytes(): ByteArray = + if (isEmpty()) ByteArray(0) + else ByteArray(length / 2) { i -> + ((this[i * 2].digitToInt(16) shl 4) or this[i * 2 + 1].digitToInt(16)).toByte() + } + + private fun ByteArray.toHexString(): String = + joinToString("") { (it.toInt() and 0xFF).toString(16).padStart(2, '0') } +} diff --git a/libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/TestResources.kt b/libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/TestResources.kt new file mode 100644 index 0000000000..625819838c --- /dev/null +++ b/libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/TestResources.kt @@ -0,0 +1,4 @@ +package com.getcode.ed25519kmp + +/** Reads a test resource file by name from `src/commonTest/resources/`. */ +expect fun readTestResource(name: String): String diff --git a/libs/encryption/ed25519-kmp/src/commonTest/resources/ed25519.json b/libs/encryption/ed25519-kmp/src/commonTest/resources/ed25519.json new file mode 100644 index 0000000000..0f3ec59714 --- /dev/null +++ b/libs/encryption/ed25519-kmp/src/commonTest/resources/ed25519.json @@ -0,0 +1,49 @@ +{ + "algorithm": "ed25519", + "spec": "RFC 8032 (SHA-512). seed=32-byte private seed; publicKey/signature are standard outputs.", + "note": "Cross-platform parity gate: both apps must reproduce publicKey and signature for each seed/message.", + "vectors": [ + { + "name": "rfc8032-test1", + "seed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "message": "", + "publicKey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "signature": "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" + }, + { + "name": "rfc8032-test2", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6f8", + "message": "72", + "publicKey": "f7cbf1b83a8ff48bd6f88cdf132b1fee4a1ffc436741f1e068d2d9c29a9308ae", + "signature": "4b5737a671f7b7f4d50848fc87277460cd65142d5752c17a2b7b10390a8803c48a52f04f37ca575fe456e632bff18de8bc32a38dec5535a874a850d18b4fe300" + }, + { + "name": "rfc8032-test3", + "seed": "c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7", + "message": "af82", + "publicKey": "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025", + "signature": "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a" + }, + { + "name": "zero-seed-empty", + "seed": "0000000000000000000000000000000000000000000000000000000000000000", + "message": "", + "publicKey": "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", + "signature": "8f895b3cafe2c9506039d0e2a66382568004674fe8d237785092e40d6aaf483e4fc60168705f31f101596138ce21aa357c0d32a064f423dc3ee4aa3abf53f803" + }, + { + "name": "zero-seed-32b", + "seed": "0000000000000000000000000000000000000000000000000000000000000000", + "message": "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + "publicKey": "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", + "signature": "b715b42bcdf6a3755d83f500103441557410920c276efb3102752f36e301ccdec2e5015ebdd6595a1844518a69b85f156db8ed9792d85f483f5c779ae2b90b0f" + }, + { + "name": "seed-ff-tx", + "seed": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "message": "0100000000000000dededededededededededededededededededededededededededededededededededededededededededededededededededededededededededededededede", + "publicKey": "76a1592044a6e4f511265bca73a604d90b0529d1df602be30a19a9257660d1f5", + "signature": "22cb95f107e77716d4084c004543abc3dfa7562fee71d3af63c844a357c75e0277a29d275f10a65c57c93c0b1d60a46cb1c49f045a2b82fbc03d113e1768c40a" + } + ] +} diff --git a/libs/encryption/ed25519-kmp/src/iosMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.ios.kt b/libs/encryption/ed25519-kmp/src/iosMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.ios.kt new file mode 100644 index 0000000000..773287e467 --- /dev/null +++ b/libs/encryption/ed25519-kmp/src/iosMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.ios.kt @@ -0,0 +1,75 @@ +package com.getcode.ed25519kmp + +import ed25519.ed25519_create_keypair +import ed25519.ed25519_on_curve +import ed25519.ed25519_sign +import ed25519.ed25519_verify +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.convert +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.readBytes +import kotlinx.cinterop.toCValues +import kotlinx.cinterop.UByteVar + +/** + * iOS actual: calls the vendored orlp/ed25519 C library directly via Kotlin/Native cinterop. + * + * Memory safety: + * - All byte arrays are pinned or copied into C-managed memory inside [memScoped]. + * - Output buffers are stack-allocated inside [memScoped] then copied to [ByteArray]. + * + * Sizes (from ed25519.h): + * publicKey = 32 bytes + * privateKey = 64 bytes (seed || public key, orlp convention) + * signature = 64 bytes + */ +@OptIn(ExperimentalForeignApi::class) +actual object Ed25519Kmp { + + actual fun createKeyPair(seed: ByteArray): KeyPair = memScoped { + require(seed.size == 32) { "seed must be 32 bytes, got ${seed.size}" } + val pubKey = allocArray(32) + val privKey = allocArray(64) + val seedPtr = seed.toUByteArray().toCValues().ptr + ed25519_create_keypair(pubKey, privKey, seedPtr) + KeyPair( + publicKey = pubKey.readBytes(32), + privateKey = privKey.readBytes(64), + ) + } + + actual fun sign( + message: ByteArray, + publicKey: ByteArray, + privateKey: ByteArray, + ): ByteArray = memScoped { + require(publicKey.size == 32) { "publicKey must be 32 bytes" } + require(privateKey.size == 64) { "privateKey must be 64 bytes" } + val sigBuf = allocArray(64) + val msgPtr = message.toUByteArray().toCValues().ptr + val pubPtr = publicKey.toUByteArray().toCValues().ptr + val privPtr = privateKey.toUByteArray().toCValues().ptr + ed25519_sign(sigBuf, msgPtr, message.size.convert(), pubPtr, privPtr) + sigBuf.readBytes(64) + } + + actual fun verify( + signature: ByteArray, + message: ByteArray, + publicKey: ByteArray, + ): Boolean = memScoped { + require(signature.size == 64) { "signature must be 64 bytes" } + require(publicKey.size == 32) { "publicKey must be 32 bytes" } + val sigPtr = signature.toUByteArray().toCValues().ptr + val msgPtr = message.toUByteArray().toCValues().ptr + val pubPtr = publicKey.toUByteArray().toCValues().ptr + ed25519_verify(sigPtr, msgPtr, message.size.convert(), pubPtr) != 0 + } + + actual fun onCurve(publicKey: ByteArray): Boolean = memScoped { + require(publicKey.size == 32) { "publicKey must be 32 bytes" } + val pubPtr = publicKey.toUByteArray().toCValues().ptr + ed25519_on_curve(pubPtr) != 0 + } +} diff --git a/libs/encryption/ed25519-kmp/src/iosTest/kotlin/com/getcode/ed25519kmp/TestResources.ios.kt b/libs/encryption/ed25519-kmp/src/iosTest/kotlin/com/getcode/ed25519kmp/TestResources.ios.kt new file mode 100644 index 0000000000..f8cb8422dc --- /dev/null +++ b/libs/encryption/ed25519-kmp/src/iosTest/kotlin/com/getcode/ed25519kmp/TestResources.ios.kt @@ -0,0 +1,17 @@ +package com.getcode.ed25519kmp + +import platform.Foundation.NSBundle +import platform.Foundation.NSString +import platform.Foundation.NSUTF8StringEncoding +import platform.Foundation.stringWithContentsOfFile + +actual fun readTestResource(name: String): String { + val nameWithoutExt = name.substringBeforeLast(".") + val ext = name.substringAfterLast(".", "") + val path = checkNotNull( + NSBundle.mainBundle.pathForResource(nameWithoutExt, ext) + ) { "Resource '$name' not found in bundle" } + return checkNotNull( + NSString.stringWithContentsOfFile(path, NSUTF8StringEncoding, null) + ) { "Failed to read resource '$name' at $path" } as String +} diff --git a/settings.gradle.kts b/settings.gradle.kts index fc23b06914..2315b874f9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -140,6 +140,7 @@ include( ":libs:emojis", ":libs:encryption:base58", ":libs:encryption:ed25519", + ":libs:encryption:ed25519-kmp", ":libs:encryption:hmac", ":libs:encryption:keys", ":libs:encryption:mnemonic", @@ -252,6 +253,7 @@ val kmpUnitTestModules = setOf( ":libs:encryption:sha256", ":libs:encryption:sha512", ":libs:encryption:hmac", + ":libs:encryption:ed25519-kmp", ) val noUnitTestModules = setOf(":apps:flipcash:benchmark", ":kmp:shared-core") val unitTestCandidates = includedProjectPaths.filter { path -> From 53cffca2ba56798e67554c856e43d022d187b8b7 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 7 Aug 2026 15:53:19 -0400 Subject: [PATCH 2/7] fix(kmp/ed25519): host test JNI libs + java.util.Base64 + cinterop flags - Replace android.util.Base64 with java.util.Base64 so androidHostTest runs on JVM without Android stubs - Add macOS arm64 dylibs (libnative-lib.dylib, libed25519.dylib) to androidHostTest/jniLibs so the JNI native-lib can load in host tests - Use definitionFile (non-deprecated) instead of defFile in cinterop DSL - Enable kotlin.mpp.enableCInteropCommonization in gradle.properties --- gradle.properties | 1 + libs/encryption/ed25519-kmp/build.gradle.kts | 2 +- .../androidHostTest/jniLibs/libed25519.dylib | Bin 0 -> 85632 bytes .../jniLibs/libnative-lib.dylib | Bin 0 -> 147760 bytes .../getcode/ed25519kmp/Ed25519Kmp.android.kt | 20 +++++++++++------- 5 files changed, 14 insertions(+), 9 deletions(-) create mode 100755 libs/encryption/ed25519-kmp/src/androidHostTest/jniLibs/libed25519.dylib create mode 100755 libs/encryption/ed25519-kmp/src/androidHostTest/jniLibs/libnative-lib.dylib diff --git a/gradle.properties b/gradle.properties index 5b07082ba9..fc1b2b20e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -50,3 +50,4 @@ android.enableR8.fullMode=true org.gradle.tooling.parallel=true kotlin.native.ignoreDisabledTargets=true +kotlin.mpp.enableCInteropCommonization=true diff --git a/libs/encryption/ed25519-kmp/build.gradle.kts b/libs/encryption/ed25519-kmp/build.gradle.kts index 39a35611d0..a939b2eeaa 100644 --- a/libs/encryption/ed25519-kmp/build.gradle.kts +++ b/libs/encryption/ed25519-kmp/build.gradle.kts @@ -83,7 +83,7 @@ kotlin { val cinteropTaskName = "cinteropEd25519${name.replaceFirstChar { it.uppercaseChar() }}" compilations["main"].cinterops.create("ed25519") { - defFile = file("cinterop/ed25519.def") + definitionFile = file("cinterop/ed25519.def") includeDirs(ed25519SrcDir) } diff --git a/libs/encryption/ed25519-kmp/src/androidHostTest/jniLibs/libed25519.dylib b/libs/encryption/ed25519-kmp/src/androidHostTest/jniLibs/libed25519.dylib new file mode 100755 index 0000000000000000000000000000000000000000..09cc85ffa92b7bedba22072ada69fee01a1b094f GIT binary patch literal 85632 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 DzSwzD literal 0 HcmV?d00001 diff --git a/libs/encryption/ed25519-kmp/src/androidHostTest/jniLibs/libnative-lib.dylib b/libs/encryption/ed25519-kmp/src/androidHostTest/jniLibs/libnative-lib.dylib new file mode 100755 index 0000000000000000000000000000000000000000..b12ffa424b5a7914c998eb995a6821d147a31b60 GIT binary patch literal 147760 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 literal 0 HcmV?d00001 diff --git a/libs/encryption/ed25519-kmp/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt b/libs/encryption/ed25519-kmp/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt index 4a06d0c253..cd991420f9 100644 --- a/libs/encryption/ed25519-kmp/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt +++ b/libs/encryption/ed25519-kmp/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt @@ -1,23 +1,27 @@ package com.getcode.ed25519kmp -import android.util.Base64 import com.getcode.ed25519.Ed25519 as JniEd25519 +import java.util.Base64 /** * Android actual: delegates to the existing JNI [com.getcode.ed25519.Ed25519]. * - * The JNI layer encodes/decodes through base64 internally; we handle that - * translation here so [commonMain] consumers always work with raw [ByteArray]. + * The JNI layer encodes/decodes through Android's Base64 internally (Base64.DEFAULT + * which adds newlines). We replicate that encoding here using [java.util.Base64] + * (available Java 8+, works in both Android runtime and JVM host tests) with the + * MIME codec which also handles newlines on decode. */ actual object Ed25519Kmp { actual fun createKeyPair(seed: ByteArray): KeyPair { - // Ed25519.createKeyPair expects a base64-encoded seed (Base64.DEFAULT). - val seedB64 = Base64.encodeToString(seed, Base64.DEFAULT) + // Ed25519.java calls Base64.encodeToString(seed, Base64.DEFAULT) internally + // for createKeyPair(byte[]). We must replicate Base64.DEFAULT encoding + // (which wraps at 76 chars) so the JNI receives the expected format. + val seedB64 = Base64.getMimeEncoder().encodeToString(seed) val jniPair = JniEd25519.createKeyPair(seedB64) - // JniPair.publicKey / privateKey are base64 strings (Base64.DEFAULT). - val publicKey = Base64.decode(jniPair.publicKey, Base64.DEFAULT) - val privateKey = Base64.decode(jniPair.privateKey, Base64.DEFAULT) + // JniPair.publicKey / privateKey are base64 strings (Base64.DEFAULT = MIME). + val publicKey = Base64.getMimeDecoder().decode(jniPair.publicKey) + val privateKey = Base64.getMimeDecoder().decode(jniPair.privateKey) return KeyPair(publicKey = publicKey, privateKey = privateKey) } From d03479d618fa05eb79ca43877c2ce6df02c0a733 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 7 Aug 2026 16:57:30 -0400 Subject: [PATCH 3/7] refactor(kmp/ed25519): rename modules + mnemonic Kotlin rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename :libs:encryption:ed25519-kmp → :libs:encryption:ed25519 (KMP module takes the clean name; no -kmp suffix exposed to consumers) - Rename :libs:encryption:ed25519 → :libs:encryption:ed25519-native (JNI/CMake module becomes an internal impl detail; androidMain delegates to it) - Update cinterop path in ed25519.def and C source reference in build.gradle.kts - Update kmpUnitTestModules and settings.gradle.kts registrations - Update :kmp:shared-core api/export references - Convert MnemonicCode and MnemonicException from Java to Kotlin (cherry-picked from origin/fix/kmp-consumer-compile) --- kmp/shared-core/build.gradle.kts | 4 +- libs/encryption/ed25519-kmp/build.gradle.kts | 121 --------- .../ed25519-kmp/cinterop/ed25519.def | 2 - .../{ed25519 => ed25519-native}/.gitignore | 0 .../CMakeLists.txt | 0 .../ed25519-native/build.gradle.kts | 30 +++ .../libs/base64/CMakeLists.txt | 0 .../libs/base64/base64.cpp | 0 .../libs/base64/base64.hpp | 0 .../libs/ed25519/CMakeLists.txt | 0 .../libs/ed25519/license.txt | 0 .../libs/ed25519/readme.md | 0 .../libs/ed25519/src/add_scalar.c | 0 .../libs/ed25519/src/ed25519.h | 0 .../libs/ed25519/src/fe.c | 0 .../libs/ed25519/src/fe.h | 0 .../libs/ed25519/src/fixedint.h | 0 .../libs/ed25519/src/ge.c | 0 .../libs/ed25519/src/ge.h | 0 .../libs/ed25519/src/key_exchange.c | 0 .../libs/ed25519/src/keypair.c | 0 .../libs/ed25519/src/precomp_data.h | 0 .../libs/ed25519/src/sc.c | 0 .../libs/ed25519/src/sc.h | 0 .../libs/ed25519/src/seed.c | 0 .../libs/ed25519/src/sha3.c | 0 .../libs/ed25519/src/sha3.h | 0 .../libs/ed25519/src/sha512.c | 0 .../libs/ed25519/src/sha512.h | 0 .../libs/ed25519/src/sign.c | 0 .../libs/ed25519/src/verify.c | 0 .../libs/ed25519/test.c | 0 .../src/androidTest/assets}/ed25519.json | 0 .../com/getcode/ed25519/Ed25519VectorTest.kt | 0 .../src/main/AndroidManifest.xml | 0 .../src/main/cpp/native-lib.cpp | 0 .../java/com/getcode/ed25519/Ed25519.java | 0 .../serializer/KeyPairAsStringSerializer.kt | 0 libs/encryption/ed25519/build.gradle.kts | 131 +++++++-- libs/encryption/ed25519/cinterop/ed25519.def | 2 + .../androidHostTest/jniLibs/libed25519.dylib | Bin .../jniLibs/libnative-lib.dylib | Bin .../ed25519kmp/TestResources.android.kt | 0 .../getcode/ed25519kmp/Ed25519Kmp.android.kt | 0 .../ed25519kmp/TestResources.android.kt | 0 .../com/getcode/ed25519kmp/Ed25519Kmp.kt | 0 .../getcode/ed25519kmp/Ed25519VectorTest.kt | 0 .../com/getcode/ed25519kmp/TestResources.kt | 0 .../resources}/ed25519.json | 0 .../com/getcode/ed25519kmp/Ed25519Kmp.ios.kt | 0 .../getcode/ed25519kmp/TestResources.ios.kt | 0 .../java/com/getcode/crypt/MnemonicCode.java | 255 ------------------ .../com/getcode/crypt/MnemonicException.java | 63 ----- .../kotlin/com/getcode/crypt/MnemonicCode.kt | 237 ++++++++++++++++ .../com/getcode/crypt/MnemonicException.kt | 33 +++ settings.gradle.kts | 4 +- 56 files changed, 417 insertions(+), 465 deletions(-) delete mode 100644 libs/encryption/ed25519-kmp/build.gradle.kts delete mode 100644 libs/encryption/ed25519-kmp/cinterop/ed25519.def rename libs/encryption/{ed25519 => ed25519-native}/.gitignore (100%) rename libs/encryption/{ed25519 => ed25519-native}/CMakeLists.txt (100%) create mode 100644 libs/encryption/ed25519-native/build.gradle.kts rename libs/encryption/{ed25519 => ed25519-native}/libs/base64/CMakeLists.txt (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/base64/base64.cpp (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/base64/base64.hpp (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/CMakeLists.txt (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/license.txt (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/readme.md (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/add_scalar.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/ed25519.h (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/fe.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/fe.h (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/fixedint.h (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/ge.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/ge.h (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/key_exchange.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/keypair.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/precomp_data.h (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/sc.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/sc.h (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/seed.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/sha3.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/sha3.h (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/sha512.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/sha512.h (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/sign.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/src/verify.c (100%) rename libs/encryption/{ed25519 => ed25519-native}/libs/ed25519/test.c (100%) rename libs/encryption/{ed25519-kmp/src/commonTest/resources => ed25519-native/src/androidTest/assets}/ed25519.json (100%) rename libs/encryption/{ed25519 => ed25519-native}/src/androidTest/java/com/getcode/ed25519/Ed25519VectorTest.kt (100%) rename libs/encryption/{ed25519 => ed25519-native}/src/main/AndroidManifest.xml (100%) rename libs/encryption/{ed25519 => ed25519-native}/src/main/cpp/native-lib.cpp (100%) rename libs/encryption/{ed25519 => ed25519-native}/src/main/java/com/getcode/ed25519/Ed25519.java (100%) rename libs/encryption/{ed25519 => ed25519-native}/src/main/java/com/getcode/utils/serializer/KeyPairAsStringSerializer.kt (100%) create mode 100644 libs/encryption/ed25519/cinterop/ed25519.def rename libs/encryption/{ed25519-kmp => ed25519}/src/androidHostTest/jniLibs/libed25519.dylib (100%) rename libs/encryption/{ed25519-kmp => ed25519}/src/androidHostTest/jniLibs/libnative-lib.dylib (100%) rename libs/encryption/{ed25519-kmp => ed25519}/src/androidHostTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt (100%) rename libs/encryption/{ed25519-kmp => ed25519}/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt (100%) rename libs/encryption/{ed25519-kmp => ed25519}/src/androidTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt (100%) rename libs/encryption/{ed25519-kmp => ed25519}/src/commonMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.kt (100%) rename libs/encryption/{ed25519-kmp => ed25519}/src/commonTest/kotlin/com/getcode/ed25519kmp/Ed25519VectorTest.kt (100%) rename libs/encryption/{ed25519-kmp => ed25519}/src/commonTest/kotlin/com/getcode/ed25519kmp/TestResources.kt (100%) rename libs/encryption/ed25519/src/{androidTest/assets => commonTest/resources}/ed25519.json (100%) rename libs/encryption/{ed25519-kmp => ed25519}/src/iosMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.ios.kt (100%) rename libs/encryption/{ed25519-kmp => ed25519}/src/iosTest/kotlin/com/getcode/ed25519kmp/TestResources.ios.kt (100%) delete mode 100644 libs/encryption/mnemonic/src/main/java/com/getcode/crypt/MnemonicCode.java delete mode 100644 libs/encryption/mnemonic/src/main/java/com/getcode/crypt/MnemonicException.java create mode 100644 libs/encryption/mnemonic/src/main/kotlin/com/getcode/crypt/MnemonicCode.kt create mode 100644 libs/encryption/mnemonic/src/main/kotlin/com/getcode/crypt/MnemonicException.kt diff --git a/kmp/shared-core/build.gradle.kts b/kmp/shared-core/build.gradle.kts index 0139999c4d..d1ba88a669 100644 --- a/kmp/shared-core/build.gradle.kts +++ b/kmp/shared-core/build.gradle.kts @@ -24,7 +24,7 @@ kotlin { export(project(":libs:encryption:sha256")) export(project(":libs:encryption:sha512")) export(project(":libs:encryption:hmac")) - export(project(":libs:encryption:ed25519-kmp")) + export(project(":libs:encryption:ed25519")) } } @@ -35,7 +35,7 @@ kotlin { api(project(":libs:encryption:sha256")) api(project(":libs:encryption:sha512")) api(project(":libs:encryption:hmac")) - api(project(":libs:encryption:ed25519-kmp")) + api(project(":libs:encryption:ed25519")) } } } diff --git a/libs/encryption/ed25519-kmp/build.gradle.kts b/libs/encryption/ed25519-kmp/build.gradle.kts deleted file mode 100644 index a939b2eeaa..0000000000 --- a/libs/encryption/ed25519-kmp/build.gradle.kts +++ /dev/null @@ -1,121 +0,0 @@ -import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget - -plugins { - kotlin("multiplatform") - id("com.android.kotlin.multiplatform.library") -} - -// ── C source paths ──────────────────────────────────────────────────────────── -// The ed25519 C sources live in the existing ed25519 JNI module. -val ed25519SrcDir = rootProject.file("libs/encryption/ed25519/libs/ed25519/src") -val ed25519CSources = fileTree(ed25519SrcDir) { - include("*.c") - // key_exchange.c is omitted — not part of our public API surface. - // seed.c is omitted — uses platform entropy; KMP API takes a caller-supplied seed. - exclude("key_exchange.c", "seed.c") -} - -// ── Static-library compilation per Apple target ─────────────────────────────── -// -// Kotlin/Native cinterop only generates Kotlin bindings from the header; it does -// not compile C sources for Apple targets. We produce a static archive -// (libored25519.a) for each target and tell the linker about it. -// -// Output: build/cinterop//libored25519.a - -data class AppleTarget(val kotlinName: String, val sdk: String, val arch: String) - -val appleTargetDefs = listOf( - AppleTarget("iosArm64", "iphoneos", "arm64"), - AppleTarget("iosSimulatorArm64", "iphonesimulator", "arm64"), - AppleTarget("iosX64", "iphonesimulator", "x86_64"), -) - -appleTargetDefs.forEach { target -> - val outDir = layout.buildDirectory.dir("cinterop/${target.kotlinName}") - tasks.register("compileEd25519C_${target.kotlinName}", Exec::class) { - group = "cinterop" - description = "Compile ed25519 C sources into a static archive for ${target.kotlinName}" - - inputs.files(ed25519CSources) - outputs.dir(outDir) - - doFirst { - outDir.get().asFile.mkdirs() - } - - // Compile each .c → .o then archive all .o into libored25519.a in one shell invocation. - commandLine("sh", "-c", buildString { - val compileLines = ed25519CSources.files.joinToString(" && ") { src -> - val obj = outDir.get().file(src.nameWithoutExtension + ".o").asFile.absolutePath - "xcrun -sdk ${target.sdk} clang -arch ${target.arch} -O2" + - " -c \"${src.absolutePath}\"" + - " -I\"${ed25519SrcDir.absolutePath}\"" + - " -o \"$obj\"" - } - val objPaths = ed25519CSources.files.joinToString(" ") { src -> - "\"${outDir.get().file(src.nameWithoutExtension + ".o").asFile.absolutePath}\"" - } - val libPath = outDir.get().file("libored25519.a").asFile.absolutePath - append(compileLines) - append(" && ar rcs \"$libPath\" $objPaths") - }) - } -} - -kotlin { - android { - namespace = "com.getcode.encryption.ed25519kmp" - compileSdk = 37 - minSdk = 29 - withHostTest {} - } - - iosArm64() - iosSimulatorArm64() - iosX64() - - // ── Cinterop + linker wiring for each Apple target ──────────────────────── - targets.withType().configureEach { - val targetDef = appleTargetDefs.first { it.kotlinName == name } - val libDir = layout.buildDirectory.dir("cinterop/$name") - val compileTaskName = "compileEd25519C_$name" - val cinteropTaskName = "cinteropEd25519${name.replaceFirstChar { it.uppercaseChar() }}" - - compilations["main"].cinterops.create("ed25519") { - definitionFile = file("cinterop/ed25519.def") - includeDirs(ed25519SrcDir) - } - - // Both the cinterop binding task and the Kotlin compile task need the - // static archive to exist before linking. - tasks.matching { it.name == cinteropTaskName }.configureEach { - dependsOn(compileTaskName) - } - tasks.matching { it.name == "compileKotlin${name.replaceFirstChar { it.uppercaseChar() }}" }.configureEach { - dependsOn(compileTaskName) - } - - binaries.configureEach { - linkerOpts("-L${libDir.get().asFile.absolutePath}", "-lored25519") - } - } - - sourceSets { - commonMain { - // Ed25519Kmp expect object + KeyPair — no external deps. - } - androidMain { - dependencies { - // Delegate to the existing JNI module — no NDK recompilation. - implementation(project(":libs:encryption:ed25519")) - } - } - commonTest { - dependencies { - implementation(kotlin("test")) - implementation(libs.kotlinx.serialization.json) - } - } - } -} diff --git a/libs/encryption/ed25519-kmp/cinterop/ed25519.def b/libs/encryption/ed25519-kmp/cinterop/ed25519.def deleted file mode 100644 index ab286c0280..0000000000 --- a/libs/encryption/ed25519-kmp/cinterop/ed25519.def +++ /dev/null @@ -1,2 +0,0 @@ -headers = ed25519.h -compilerOpts = -I../../libs/encryption/ed25519/libs/ed25519/src diff --git a/libs/encryption/ed25519/.gitignore b/libs/encryption/ed25519-native/.gitignore similarity index 100% rename from libs/encryption/ed25519/.gitignore rename to libs/encryption/ed25519-native/.gitignore diff --git a/libs/encryption/ed25519/CMakeLists.txt b/libs/encryption/ed25519-native/CMakeLists.txt similarity index 100% rename from libs/encryption/ed25519/CMakeLists.txt rename to libs/encryption/ed25519-native/CMakeLists.txt diff --git a/libs/encryption/ed25519-native/build.gradle.kts b/libs/encryption/ed25519-native/build.gradle.kts new file mode 100644 index 0000000000..8514da255a --- /dev/null +++ b/libs/encryption/ed25519-native/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + alias(libs.plugins.flipcash.android.library) +} + +android { + namespace = "${Gradle.codeNamespace}.ed25519" + ndkVersion = "29.0.14206865" + defaultConfig { + externalNativeBuild { + cmake { + cppFlags += "-std=c++11" + } + } + } + + externalNativeBuild { + cmake { + path = file("CMakeLists.txt") + } + } +} + +dependencies { + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + implementation(libs.bundles.kotlinx.serialization) + + // Cross-platform test-vector gate (instrumented: JNI + android.util.Base64 need a device). + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.test.runner) +} diff --git a/libs/encryption/ed25519/libs/base64/CMakeLists.txt b/libs/encryption/ed25519-native/libs/base64/CMakeLists.txt similarity index 100% rename from libs/encryption/ed25519/libs/base64/CMakeLists.txt rename to libs/encryption/ed25519-native/libs/base64/CMakeLists.txt diff --git a/libs/encryption/ed25519/libs/base64/base64.cpp b/libs/encryption/ed25519-native/libs/base64/base64.cpp similarity index 100% rename from libs/encryption/ed25519/libs/base64/base64.cpp rename to libs/encryption/ed25519-native/libs/base64/base64.cpp diff --git a/libs/encryption/ed25519/libs/base64/base64.hpp b/libs/encryption/ed25519-native/libs/base64/base64.hpp similarity index 100% rename from libs/encryption/ed25519/libs/base64/base64.hpp rename to libs/encryption/ed25519-native/libs/base64/base64.hpp diff --git a/libs/encryption/ed25519/libs/ed25519/CMakeLists.txt b/libs/encryption/ed25519-native/libs/ed25519/CMakeLists.txt similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/CMakeLists.txt rename to libs/encryption/ed25519-native/libs/ed25519/CMakeLists.txt diff --git a/libs/encryption/ed25519/libs/ed25519/license.txt b/libs/encryption/ed25519-native/libs/ed25519/license.txt similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/license.txt rename to libs/encryption/ed25519-native/libs/ed25519/license.txt diff --git a/libs/encryption/ed25519/libs/ed25519/readme.md b/libs/encryption/ed25519-native/libs/ed25519/readme.md similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/readme.md rename to libs/encryption/ed25519-native/libs/ed25519/readme.md diff --git a/libs/encryption/ed25519/libs/ed25519/src/add_scalar.c b/libs/encryption/ed25519-native/libs/ed25519/src/add_scalar.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/add_scalar.c rename to libs/encryption/ed25519-native/libs/ed25519/src/add_scalar.c diff --git a/libs/encryption/ed25519/libs/ed25519/src/ed25519.h b/libs/encryption/ed25519-native/libs/ed25519/src/ed25519.h similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/ed25519.h rename to libs/encryption/ed25519-native/libs/ed25519/src/ed25519.h diff --git a/libs/encryption/ed25519/libs/ed25519/src/fe.c b/libs/encryption/ed25519-native/libs/ed25519/src/fe.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/fe.c rename to libs/encryption/ed25519-native/libs/ed25519/src/fe.c diff --git a/libs/encryption/ed25519/libs/ed25519/src/fe.h b/libs/encryption/ed25519-native/libs/ed25519/src/fe.h similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/fe.h rename to libs/encryption/ed25519-native/libs/ed25519/src/fe.h diff --git a/libs/encryption/ed25519/libs/ed25519/src/fixedint.h b/libs/encryption/ed25519-native/libs/ed25519/src/fixedint.h similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/fixedint.h rename to libs/encryption/ed25519-native/libs/ed25519/src/fixedint.h diff --git a/libs/encryption/ed25519/libs/ed25519/src/ge.c b/libs/encryption/ed25519-native/libs/ed25519/src/ge.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/ge.c rename to libs/encryption/ed25519-native/libs/ed25519/src/ge.c diff --git a/libs/encryption/ed25519/libs/ed25519/src/ge.h b/libs/encryption/ed25519-native/libs/ed25519/src/ge.h similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/ge.h rename to libs/encryption/ed25519-native/libs/ed25519/src/ge.h diff --git a/libs/encryption/ed25519/libs/ed25519/src/key_exchange.c b/libs/encryption/ed25519-native/libs/ed25519/src/key_exchange.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/key_exchange.c rename to libs/encryption/ed25519-native/libs/ed25519/src/key_exchange.c diff --git a/libs/encryption/ed25519/libs/ed25519/src/keypair.c b/libs/encryption/ed25519-native/libs/ed25519/src/keypair.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/keypair.c rename to libs/encryption/ed25519-native/libs/ed25519/src/keypair.c diff --git a/libs/encryption/ed25519/libs/ed25519/src/precomp_data.h b/libs/encryption/ed25519-native/libs/ed25519/src/precomp_data.h similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/precomp_data.h rename to libs/encryption/ed25519-native/libs/ed25519/src/precomp_data.h diff --git a/libs/encryption/ed25519/libs/ed25519/src/sc.c b/libs/encryption/ed25519-native/libs/ed25519/src/sc.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/sc.c rename to libs/encryption/ed25519-native/libs/ed25519/src/sc.c diff --git a/libs/encryption/ed25519/libs/ed25519/src/sc.h b/libs/encryption/ed25519-native/libs/ed25519/src/sc.h similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/sc.h rename to libs/encryption/ed25519-native/libs/ed25519/src/sc.h diff --git a/libs/encryption/ed25519/libs/ed25519/src/seed.c b/libs/encryption/ed25519-native/libs/ed25519/src/seed.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/seed.c rename to libs/encryption/ed25519-native/libs/ed25519/src/seed.c diff --git a/libs/encryption/ed25519/libs/ed25519/src/sha3.c b/libs/encryption/ed25519-native/libs/ed25519/src/sha3.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/sha3.c rename to libs/encryption/ed25519-native/libs/ed25519/src/sha3.c diff --git a/libs/encryption/ed25519/libs/ed25519/src/sha3.h b/libs/encryption/ed25519-native/libs/ed25519/src/sha3.h similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/sha3.h rename to libs/encryption/ed25519-native/libs/ed25519/src/sha3.h diff --git a/libs/encryption/ed25519/libs/ed25519/src/sha512.c b/libs/encryption/ed25519-native/libs/ed25519/src/sha512.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/sha512.c rename to libs/encryption/ed25519-native/libs/ed25519/src/sha512.c diff --git a/libs/encryption/ed25519/libs/ed25519/src/sha512.h b/libs/encryption/ed25519-native/libs/ed25519/src/sha512.h similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/sha512.h rename to libs/encryption/ed25519-native/libs/ed25519/src/sha512.h diff --git a/libs/encryption/ed25519/libs/ed25519/src/sign.c b/libs/encryption/ed25519-native/libs/ed25519/src/sign.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/sign.c rename to libs/encryption/ed25519-native/libs/ed25519/src/sign.c diff --git a/libs/encryption/ed25519/libs/ed25519/src/verify.c b/libs/encryption/ed25519-native/libs/ed25519/src/verify.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/src/verify.c rename to libs/encryption/ed25519-native/libs/ed25519/src/verify.c diff --git a/libs/encryption/ed25519/libs/ed25519/test.c b/libs/encryption/ed25519-native/libs/ed25519/test.c similarity index 100% rename from libs/encryption/ed25519/libs/ed25519/test.c rename to libs/encryption/ed25519-native/libs/ed25519/test.c diff --git a/libs/encryption/ed25519-kmp/src/commonTest/resources/ed25519.json b/libs/encryption/ed25519-native/src/androidTest/assets/ed25519.json similarity index 100% rename from libs/encryption/ed25519-kmp/src/commonTest/resources/ed25519.json rename to libs/encryption/ed25519-native/src/androidTest/assets/ed25519.json diff --git a/libs/encryption/ed25519/src/androidTest/java/com/getcode/ed25519/Ed25519VectorTest.kt b/libs/encryption/ed25519-native/src/androidTest/java/com/getcode/ed25519/Ed25519VectorTest.kt similarity index 100% rename from libs/encryption/ed25519/src/androidTest/java/com/getcode/ed25519/Ed25519VectorTest.kt rename to libs/encryption/ed25519-native/src/androidTest/java/com/getcode/ed25519/Ed25519VectorTest.kt diff --git a/libs/encryption/ed25519/src/main/AndroidManifest.xml b/libs/encryption/ed25519-native/src/main/AndroidManifest.xml similarity index 100% rename from libs/encryption/ed25519/src/main/AndroidManifest.xml rename to libs/encryption/ed25519-native/src/main/AndroidManifest.xml diff --git a/libs/encryption/ed25519/src/main/cpp/native-lib.cpp b/libs/encryption/ed25519-native/src/main/cpp/native-lib.cpp similarity index 100% rename from libs/encryption/ed25519/src/main/cpp/native-lib.cpp rename to libs/encryption/ed25519-native/src/main/cpp/native-lib.cpp diff --git a/libs/encryption/ed25519/src/main/java/com/getcode/ed25519/Ed25519.java b/libs/encryption/ed25519-native/src/main/java/com/getcode/ed25519/Ed25519.java similarity index 100% rename from libs/encryption/ed25519/src/main/java/com/getcode/ed25519/Ed25519.java rename to libs/encryption/ed25519-native/src/main/java/com/getcode/ed25519/Ed25519.java diff --git a/libs/encryption/ed25519/src/main/java/com/getcode/utils/serializer/KeyPairAsStringSerializer.kt b/libs/encryption/ed25519-native/src/main/java/com/getcode/utils/serializer/KeyPairAsStringSerializer.kt similarity index 100% rename from libs/encryption/ed25519/src/main/java/com/getcode/utils/serializer/KeyPairAsStringSerializer.kt rename to libs/encryption/ed25519-native/src/main/java/com/getcode/utils/serializer/KeyPairAsStringSerializer.kt diff --git a/libs/encryption/ed25519/build.gradle.kts b/libs/encryption/ed25519/build.gradle.kts index 8514da255a..6b5a0d12b9 100644 --- a/libs/encryption/ed25519/build.gradle.kts +++ b/libs/encryption/ed25519/build.gradle.kts @@ -1,30 +1,121 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + plugins { - alias(libs.plugins.flipcash.android.library) + kotlin("multiplatform") + id("com.android.kotlin.multiplatform.library") } -android { - namespace = "${Gradle.codeNamespace}.ed25519" - ndkVersion = "29.0.14206865" - defaultConfig { - externalNativeBuild { - cmake { - cppFlags += "-std=c++11" - } - } - } +// ── C source paths ──────────────────────────────────────────────────────────── +// The ed25519 C sources live in the existing ed25519 JNI module. +val ed25519SrcDir = rootProject.file("libs/encryption/ed25519-native/libs/ed25519/src") +val ed25519CSources = fileTree(ed25519SrcDir) { + include("*.c") + // key_exchange.c is omitted — not part of our public API surface. + // seed.c is omitted — uses platform entropy; KMP API takes a caller-supplied seed. + exclude("key_exchange.c", "seed.c") +} + +// ── Static-library compilation per Apple target ─────────────────────────────── +// +// Kotlin/Native cinterop only generates Kotlin bindings from the header; it does +// not compile C sources for Apple targets. We produce a static archive +// (libored25519.a) for each target and tell the linker about it. +// +// Output: build/cinterop//libored25519.a + +data class AppleTarget(val kotlinName: String, val sdk: String, val arch: String) + +val appleTargetDefs = listOf( + AppleTarget("iosArm64", "iphoneos", "arm64"), + AppleTarget("iosSimulatorArm64", "iphonesimulator", "arm64"), + AppleTarget("iosX64", "iphonesimulator", "x86_64"), +) - externalNativeBuild { - cmake { - path = file("CMakeLists.txt") +appleTargetDefs.forEach { target -> + val outDir = layout.buildDirectory.dir("cinterop/${target.kotlinName}") + tasks.register("compileEd25519C_${target.kotlinName}", Exec::class) { + group = "cinterop" + description = "Compile ed25519 C sources into a static archive for ${target.kotlinName}" + + inputs.files(ed25519CSources) + outputs.dir(outDir) + + doFirst { + outDir.get().asFile.mkdirs() } + + // Compile each .c → .o then archive all .o into libored25519.a in one shell invocation. + commandLine("sh", "-c", buildString { + val compileLines = ed25519CSources.files.joinToString(" && ") { src -> + val obj = outDir.get().file(src.nameWithoutExtension + ".o").asFile.absolutePath + "xcrun -sdk ${target.sdk} clang -arch ${target.arch} -O2" + + " -c \"${src.absolutePath}\"" + + " -I\"${ed25519SrcDir.absolutePath}\"" + + " -o \"$obj\"" + } + val objPaths = ed25519CSources.files.joinToString(" ") { src -> + "\"${outDir.get().file(src.nameWithoutExtension + ".o").asFile.absolutePath}\"" + } + val libPath = outDir.get().file("libored25519.a").asFile.absolutePath + append(compileLines) + append(" && ar rcs \"$libPath\" $objPaths") + }) } } -dependencies { - implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) - implementation(libs.bundles.kotlinx.serialization) +kotlin { + android { + namespace = "com.getcode.encryption.ed25519" + compileSdk = 37 + minSdk = 29 + withHostTest {} + } + + iosArm64() + iosSimulatorArm64() + iosX64() - // Cross-platform test-vector gate (instrumented: JNI + android.util.Base64 need a device). - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.test.runner) + // ── Cinterop + linker wiring for each Apple target ──────────────────────── + targets.withType().configureEach { + val targetDef = appleTargetDefs.first { it.kotlinName == name } + val libDir = layout.buildDirectory.dir("cinterop/$name") + val compileTaskName = "compileEd25519C_$name" + val cinteropTaskName = "cinteropEd25519${name.replaceFirstChar { it.uppercaseChar() }}" + + compilations["main"].cinterops.create("ed25519") { + definitionFile = file("cinterop/ed25519.def") + includeDirs(ed25519SrcDir) + } + + // Both the cinterop binding task and the Kotlin compile task need the + // static archive to exist before linking. + tasks.matching { it.name == cinteropTaskName }.configureEach { + dependsOn(compileTaskName) + } + tasks.matching { it.name == "compileKotlin${name.replaceFirstChar { it.uppercaseChar() }}" }.configureEach { + dependsOn(compileTaskName) + } + + binaries.configureEach { + linkerOpts("-L${libDir.get().asFile.absolutePath}", "-lored25519") + } + } + + sourceSets { + commonMain { + // Ed25519Kmp expect object + KeyPair — no external deps. + } + androidMain { + dependencies { + // Delegate to the JNI module for NDK/CMake compilation. + implementation(project(":libs:encryption:ed25519-native")) + } + } + commonTest { + dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.serialization.json) + } + } + } } diff --git a/libs/encryption/ed25519/cinterop/ed25519.def b/libs/encryption/ed25519/cinterop/ed25519.def new file mode 100644 index 0000000000..fddbb129f8 --- /dev/null +++ b/libs/encryption/ed25519/cinterop/ed25519.def @@ -0,0 +1,2 @@ +headers = ed25519.h +compilerOpts = -I../../libs/encryption/ed25519-native/libs/ed25519/src diff --git a/libs/encryption/ed25519-kmp/src/androidHostTest/jniLibs/libed25519.dylib b/libs/encryption/ed25519/src/androidHostTest/jniLibs/libed25519.dylib similarity index 100% rename from libs/encryption/ed25519-kmp/src/androidHostTest/jniLibs/libed25519.dylib rename to libs/encryption/ed25519/src/androidHostTest/jniLibs/libed25519.dylib diff --git a/libs/encryption/ed25519-kmp/src/androidHostTest/jniLibs/libnative-lib.dylib b/libs/encryption/ed25519/src/androidHostTest/jniLibs/libnative-lib.dylib similarity index 100% rename from libs/encryption/ed25519-kmp/src/androidHostTest/jniLibs/libnative-lib.dylib rename to libs/encryption/ed25519/src/androidHostTest/jniLibs/libnative-lib.dylib diff --git a/libs/encryption/ed25519-kmp/src/androidHostTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt b/libs/encryption/ed25519/src/androidHostTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt similarity index 100% rename from libs/encryption/ed25519-kmp/src/androidHostTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt rename to libs/encryption/ed25519/src/androidHostTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt diff --git a/libs/encryption/ed25519-kmp/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt b/libs/encryption/ed25519/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt similarity index 100% rename from libs/encryption/ed25519-kmp/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt rename to libs/encryption/ed25519/src/androidMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.android.kt diff --git a/libs/encryption/ed25519-kmp/src/androidTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt b/libs/encryption/ed25519/src/androidTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt similarity index 100% rename from libs/encryption/ed25519-kmp/src/androidTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt rename to libs/encryption/ed25519/src/androidTest/kotlin/com/getcode/ed25519kmp/TestResources.android.kt diff --git a/libs/encryption/ed25519-kmp/src/commonMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.kt b/libs/encryption/ed25519/src/commonMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.kt similarity index 100% rename from libs/encryption/ed25519-kmp/src/commonMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.kt rename to libs/encryption/ed25519/src/commonMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.kt diff --git a/libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/Ed25519VectorTest.kt b/libs/encryption/ed25519/src/commonTest/kotlin/com/getcode/ed25519kmp/Ed25519VectorTest.kt similarity index 100% rename from libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/Ed25519VectorTest.kt rename to libs/encryption/ed25519/src/commonTest/kotlin/com/getcode/ed25519kmp/Ed25519VectorTest.kt diff --git a/libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/TestResources.kt b/libs/encryption/ed25519/src/commonTest/kotlin/com/getcode/ed25519kmp/TestResources.kt similarity index 100% rename from libs/encryption/ed25519-kmp/src/commonTest/kotlin/com/getcode/ed25519kmp/TestResources.kt rename to libs/encryption/ed25519/src/commonTest/kotlin/com/getcode/ed25519kmp/TestResources.kt diff --git a/libs/encryption/ed25519/src/androidTest/assets/ed25519.json b/libs/encryption/ed25519/src/commonTest/resources/ed25519.json similarity index 100% rename from libs/encryption/ed25519/src/androidTest/assets/ed25519.json rename to libs/encryption/ed25519/src/commonTest/resources/ed25519.json diff --git a/libs/encryption/ed25519-kmp/src/iosMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.ios.kt b/libs/encryption/ed25519/src/iosMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.ios.kt similarity index 100% rename from libs/encryption/ed25519-kmp/src/iosMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.ios.kt rename to libs/encryption/ed25519/src/iosMain/kotlin/com/getcode/ed25519kmp/Ed25519Kmp.ios.kt diff --git a/libs/encryption/ed25519-kmp/src/iosTest/kotlin/com/getcode/ed25519kmp/TestResources.ios.kt b/libs/encryption/ed25519/src/iosTest/kotlin/com/getcode/ed25519kmp/TestResources.ios.kt similarity index 100% rename from libs/encryption/ed25519-kmp/src/iosTest/kotlin/com/getcode/ed25519kmp/TestResources.ios.kt rename to libs/encryption/ed25519/src/iosTest/kotlin/com/getcode/ed25519kmp/TestResources.ios.kt diff --git a/libs/encryption/mnemonic/src/main/java/com/getcode/crypt/MnemonicCode.java b/libs/encryption/mnemonic/src/main/java/com/getcode/crypt/MnemonicCode.java deleted file mode 100644 index b4ae23c579..0000000000 --- a/libs/encryption/mnemonic/src/main/java/com/getcode/crypt/MnemonicCode.java +++ /dev/null @@ -1,255 +0,0 @@ -package com.getcode.crypt; - -/* - * Copyright 2013 Ken Sedgwick - * Copyright 2014 Andreas Schildbach - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.content.res.Resources; - -import com.getcode.encryption.mnemonic.R; -import com.getcode.utils.Utils; -import com.google.common.base.Stopwatch; - -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static com.google.common.base.Preconditions.checkNotNull; - -import timber.log.Timber; - -/** - * A MnemonicCode object may be used to convert between binary seed values and - * lists of words per the BIP 39 - * specification - */ - -public class MnemonicCode { - public static final String TAG = "MnemonicCode"; - - public ArrayList wordList; - - private static final String BIP39_ENGLISH_RESOURCE_NAME = "english.txt"; - private static final String BIP39_ENGLISH_SHA256 = "ad90bf3beb7b0eb7e5acd74727dc0da96e0a280a258354e7293fb7e211ac03db"; - - /** UNIX time for when the BIP39 standard was finalised. This can be used as a default seed birthday. */ - public static long BIP39_STANDARDISATION_TIME_SECS = 1381276800; - - private static final int PBKDF2_ROUNDS = 2048; - - public static MnemonicCode INSTANCE; - - static { - try { - INSTANCE = new MnemonicCode(null); - } catch (FileNotFoundException e) { - // We expect failure on Android. The developer has to set INSTANCE themselves. - if (!Utils.isAndroidRuntime()) - Timber.e("Could not find word list"); - } catch (IOException e) { - Timber.e("Failed to load word list"); - } - } - - /** Initialise from the included word list. Won't work on Android. */ - public MnemonicCode(Resources resources) throws IOException { - this(openDefaultWords(resources), BIP39_ENGLISH_SHA256); - } - - private static InputStream openDefaultWords(Resources resources) throws IOException { - if (resources == null) return null; - InputStream stream = resources.openRawResource(R.raw.english); - if (stream == null) - throw new FileNotFoundException(BIP39_ENGLISH_RESOURCE_NAME); - return stream; - } - - /** - * Creates an MnemonicCode object, initializing with words read from the supplied input stream. If a wordListDigest - * is supplied the digest of the words will be checked. - */ - public MnemonicCode(InputStream wordstream, String wordListDigest) throws IOException, IllegalArgumentException { - if (wordstream == null) return; - BufferedReader br = new BufferedReader(new InputStreamReader(wordstream, StandardCharsets.UTF_8)); - this.wordList = new ArrayList<>(2048); - MessageDigest md; - try { md = MessageDigest.getInstance("SHA-256"); } - catch (java.security.NoSuchAlgorithmException e) { throw new RuntimeException(e); } - String word; - while ((word = br.readLine()) != null) { - md.update(word.getBytes()); - this.wordList.add(word); - } - br.close(); - - if (this.wordList.size() != 2048) - throw new IllegalArgumentException("input stream did not contain 2048 words"); - - // If a wordListDigest is supplied check to make sure it matches. - if (wordListDigest != null) { - byte[] digest = md.digest(); - String hexdigest = Utils.HEX.encode(digest); - if (!hexdigest.equals(wordListDigest)) - throw new IllegalArgumentException("wordlist digest mismatch"); - } - } - - /** - * Gets the word list this code uses. - */ - public List getWordList() { - return wordList; - } - - /** - * Convert mnemonic word list to seed. - */ - public static byte[] toSeed(List words, String passphrase) { - checkNotNull(passphrase, "A null passphrase is not allowed."); - - // To create binary seed from mnemonic, we use PBKDF2 function - // with mnemonic sentence (in UTF-8) used as a password and - // string "mnemonic" + passphrase (again in UTF-8) used as a - // salt. Iteration count is set to 4096 and HMAC-SHA512 is - // used as a pseudo-random function. Desired length of the - // derived key is 512 bits (= 64 bytes). - // - String pass = Utils.spaceJoin(words); - String salt = "mnemonic" + passphrase; - - final Stopwatch watch = Stopwatch.createStarted(); - byte[] seed = PBKDF2SHA512.derive(pass, salt, PBKDF2_ROUNDS, 64); - watch.stop(); - Timber.i("PBKDF2 took {} %s", watch); - return seed; - } - - /** - * Convert mnemonic word list to original entropy value. - */ - public byte[] toEntropy(List words) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException, MnemonicException.MnemonicChecksumException { - if (words.size() % 3 > 0) - throw new MnemonicException.MnemonicLengthException("Word list size must be multiple of three words."); - - if (words.isEmpty()) - throw new MnemonicException.MnemonicLengthException("Word list is empty."); - - // Look up all the words in the list and construct the - // concatenation of the original entropy and the checksum. - // - int concatLenBits = words.size() * 11; - boolean[] concatBits = new boolean[concatLenBits]; - int wordindex = 0; - for (String word : words) { - // Find the words index in the wordlist. - int ndx = Collections.binarySearch(this.wordList, word); - if (ndx < 0) - throw new MnemonicException.MnemonicWordException(word); - - // Set the next 11 bits to the value of the index. - for (int ii = 0; ii < 11; ++ii) - concatBits[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0; - ++wordindex; - } - - int checksumLengthBits = concatLenBits / 33; - int entropyLengthBits = concatLenBits - checksumLengthBits; - - // Extract original entropy as bytes. - byte[] entropy = new byte[entropyLengthBits / 8]; - for (int ii = 0; ii < entropy.length; ++ii) - for (int jj = 0; jj < 8; ++jj) - if (concatBits[(ii * 8) + jj]) - entropy[ii] |= (byte) (1 << (7 - jj)); - - // Take the digest of the entropy. - byte[] hash = Sha256Hash.hash(entropy); - boolean[] hashBits = bytesToBits(hash); - - // Check all the checksum bits. - for (int i = 0; i < checksumLengthBits; ++i) - if (concatBits[entropyLengthBits + i] != hashBits[i]) - throw new MnemonicException.MnemonicChecksumException(); - - return entropy; - } - - /** - * Convert entropy data to mnemonic word list. - */ - public List toMnemonic(byte[] entropy) throws MnemonicException.MnemonicLengthException { - if (entropy.length % 4 > 0) - throw new MnemonicException.MnemonicLengthException("Entropy length not multiple of 32 bits."); - - if (entropy.length == 0) - throw new MnemonicException.MnemonicLengthException("Entropy is empty."); - - // We take initial entropy of ENT bits and compute its - // checksum by taking first ENT / 32 bits of its SHA256 hash. - - byte[] hash = Sha256Hash.hash(entropy); - boolean[] hashBits = bytesToBits(hash); - - boolean[] entropyBits = bytesToBits(entropy); - int checksumLengthBits = entropyBits.length / 32; - - // We append these bits to the end of the initial entropy. - boolean[] concatBits = new boolean[entropyBits.length + checksumLengthBits]; - System.arraycopy(entropyBits, 0, concatBits, 0, entropyBits.length); - System.arraycopy(hashBits, 0, concatBits, entropyBits.length, checksumLengthBits); - - // Next we take these concatenated bits and split them into - // groups of 11 bits. Each group encodes number from 0-2047 - // which is a position in a wordlist. We convert numbers into - // words and use joined words as mnemonic sentence. - - ArrayList words = new ArrayList<>(); - int nwords = concatBits.length / 11; - for (int i = 0; i < nwords; ++i) { - int index = 0; - for (int j = 0; j < 11; ++j) { - index <<= 1; - if (concatBits[(i * 11) + j]) - index |= 0x1; - } - words.add(this.wordList.get(index)); - } - - return words; - } - - /** - * Check to see if a mnemonic word list is valid. - */ - public void check(List words) throws MnemonicException { - toEntropy(words); - } - - private static boolean[] bytesToBits(byte[] data) { - boolean[] bits = new boolean[data.length * 8]; - for (int i = 0; i < data.length; ++i) - for (int j = 0; j < 8; ++j) - bits[(i * 8) + j] = (data[i] & (1 << (7 - j))) != 0; - return bits; - } -} diff --git a/libs/encryption/mnemonic/src/main/java/com/getcode/crypt/MnemonicException.java b/libs/encryption/mnemonic/src/main/java/com/getcode/crypt/MnemonicException.java deleted file mode 100644 index 37b6ab7e2f..0000000000 --- a/libs/encryption/mnemonic/src/main/java/com/getcode/crypt/MnemonicException.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.getcode.crypt; - -/* - * Copyright 2013 Ken Sedgwick - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -/** - * Exceptions thrown by the MnemonicCode module. - */ -@SuppressWarnings("serial") -public class MnemonicException extends Exception { - public MnemonicException() { - super(); - } - - public MnemonicException(String msg) { - super(msg); - } - - /** - * Thrown when an argument to MnemonicCode is the wrong length. - */ - public static class MnemonicLengthException extends MnemonicException { - public MnemonicLengthException(String msg) { - super(msg); - } - } - - /** - * Thrown when a list of MnemonicCode words fails the checksum check. - */ - public static class MnemonicChecksumException extends MnemonicException { - public MnemonicChecksumException() { - super(); - } - } - - /** - * Thrown when a word is encountered which is not in the MnemonicCode's word list. - */ - public static class MnemonicWordException extends MnemonicException { - /** Contains the word that was not found in the word list. */ - public final String badWord; - - public MnemonicWordException(String badWord) { - super(); - this.badWord = badWord; - } - } -} diff --git a/libs/encryption/mnemonic/src/main/kotlin/com/getcode/crypt/MnemonicCode.kt b/libs/encryption/mnemonic/src/main/kotlin/com/getcode/crypt/MnemonicCode.kt new file mode 100644 index 0000000000..3a305e20da --- /dev/null +++ b/libs/encryption/mnemonic/src/main/kotlin/com/getcode/crypt/MnemonicCode.kt @@ -0,0 +1,237 @@ +package com.getcode.crypt + +/* + * Copyright 2013 Ken Sedgwick + * Copyright 2014 Andreas Schildbach + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.content.res.Resources +import com.getcode.encryption.mnemonic.R +import com.getcode.utils.Utils +import com.google.common.base.Stopwatch +import timber.log.Timber +import java.io.BufferedReader +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.security.MessageDigest + +/** + * Converts between binary seed values and lists of words per the + * [BIP 39 specification](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki). + */ +class MnemonicCode { + + var wordList: ArrayList = ArrayList() + private set + + /** + * Initialises from the included word list (requires an [android.content.res.Resources] + * instance to open the raw resource; not usable on bare JVM). + */ + @Throws(IOException::class) + constructor(resources: Resources?) { + val stream = openDefaultWords(resources) ?: return + init(stream, BIP39_ENGLISH_SHA256) + } + + /** + * Initialises with words read from [wordstream]. If [wordListDigest] is non-null the + * SHA-256 hex digest of the word list is verified against it. + */ + @Throws(IOException::class, IllegalArgumentException::class) + constructor(wordstream: InputStream?, wordListDigest: String?) { + wordstream ?: return + init(wordstream, wordListDigest) + } + + @Throws(IOException::class) + private fun init(wordstream: InputStream, wordListDigest: String?) { + val br = BufferedReader(InputStreamReader(wordstream, Charsets.UTF_8)) + val list = ArrayList(2048) + val md: MessageDigest = MessageDigest.getInstance("SHA-256") + br.forEachLine { word -> + md.update(word.toByteArray()) + list.add(word) + } + br.close() + + if (list.size != 2048) throw IllegalArgumentException("input stream did not contain 2048 words") + + if (wordListDigest != null) { + val hexDigest = md.digest().toHexString() + if (hexDigest != wordListDigest) throw IllegalArgumentException("wordlist digest mismatch") + } + + wordList = list + } + + /** Returns the word list this instance uses. */ + fun getWordList(): List = wordList + + /** + * Converts mnemonic word list to original entropy bytes. + * + * @throws MnemonicException.MnemonicLengthException if the word count is not a multiple of 3 or the list is empty + * @throws MnemonicException.MnemonicWordException if a word is not in the word list + * @throws MnemonicException.MnemonicChecksumException if the checksum does not match + */ + @Throws(MnemonicException::class) + fun toEntropy(words: List): ByteArray { + if (words.size % 3 != 0) + throw MnemonicException.MnemonicLengthException("Word list size must be multiple of three words.") + if (words.isEmpty()) + throw MnemonicException.MnemonicLengthException("Word list is empty.") + + val concatLenBits = words.size * 11 + val concatBits = BooleanArray(concatLenBits) + var wordIndex = 0 + for (word in words) { + val ndx = wordList.binarySearch(word) + if (ndx < 0) throw MnemonicException.MnemonicWordException(word) + for (ii in 0 until 11) + concatBits[wordIndex * 11 + ii] = (ndx and (1 shl (10 - ii))) != 0 + wordIndex++ + } + + val checksumLengthBits = concatLenBits / 33 + val entropyLengthBits = concatLenBits - checksumLengthBits + + val entropy = ByteArray(entropyLengthBits / 8) + for (ii in entropy.indices) + for (jj in 0 until 8) + if (concatBits[ii * 8 + jj]) + entropy[ii] = (entropy[ii].toInt() or (1 shl (7 - jj))).toByte() + + val hash = Sha256Hash.hash(entropy) + val hashBits = bytesToBits(hash) + + for (i in 0 until checksumLengthBits) + if (concatBits[entropyLengthBits + i] != hashBits[i]) + throw MnemonicException.MnemonicChecksumException() + + return entropy + } + + /** + * Converts entropy bytes to a mnemonic word list. + * + * @throws MnemonicException.MnemonicLengthException if the entropy length is not a multiple of 4 or is empty + */ + @Throws(MnemonicException.MnemonicLengthException::class) + fun toMnemonic(entropy: ByteArray): List { + if (entropy.size % 4 != 0) + throw MnemonicException.MnemonicLengthException("Entropy length not multiple of 32 bits.") + if (entropy.isEmpty()) + throw MnemonicException.MnemonicLengthException("Entropy is empty.") + + val hash = Sha256Hash.hash(entropy) + val hashBits = bytesToBits(hash) + val entropyBits = bytesToBits(entropy) + val checksumLengthBits = entropyBits.size / 32 + + val concatBits = BooleanArray(entropyBits.size + checksumLengthBits) + entropyBits.copyInto(concatBits, destinationOffset = 0) + hashBits.copyInto(concatBits, destinationOffset = entropyBits.size, endIndex = checksumLengthBits) + + val words = ArrayList() + val nwords = concatBits.size / 11 + for (i in 0 until nwords) { + var index = 0 + for (j in 0 until 11) { + index = index shl 1 + if (concatBits[i * 11 + j]) index = index or 0x1 + } + words.add(wordList[index]) + } + return words + } + + /** + * Checks whether a mnemonic word list is valid. + * + * @throws MnemonicException if validation fails + */ + @Throws(MnemonicException::class) + fun check(words: List) { + toEntropy(words) + } + + companion object { + const val TAG: String = "MnemonicCode" + + private const val BIP39_ENGLISH_RESOURCE_NAME = "english.txt" + private const val BIP39_ENGLISH_SHA256 = "ad90bf3beb7b0eb7e5acd74727dc0da96e0a280a258354e7293fb7e211ac03db" + + /** UNIX time for when the BIP39 standard was finalised. Use as a default seed birthday. */ + @JvmField + var BIP39_STANDARDISATION_TIME_SECS: Long = 1381276800L + + private const val PBKDF2_ROUNDS = 2048 + + /** Shared instance (null until set, e.g. via [MnemonicCache.init]). */ + @JvmField + var INSTANCE: MnemonicCode? = null + + init { + try { + INSTANCE = MnemonicCode(null as Resources?) + } catch (e: FileNotFoundException) { + if (!Utils.isAndroidRuntime()) Timber.e("Could not find word list") + } catch (e: IOException) { + Timber.e("Failed to load word list") + } + } + + /** Converts a mnemonic word list to a 64-byte PBKDF2-SHA512 seed. */ + @JvmStatic + fun toSeed(words: List, passphrase: String): ByteArray { + requireNotNull(passphrase) { "A null passphrase is not allowed." } + val pass = words.joinToString(" ") + val salt = "mnemonic$passphrase" + val watch = Stopwatch.createStarted() + val seed = PBKDF2SHA512.derive(pass, salt, PBKDF2_ROUNDS, 64) + watch.stop() + Timber.i("PBKDF2 took {} %s", watch) + return seed + } + + private fun openDefaultWords(resources: Resources?): InputStream? { + resources ?: return null + val stream = resources.openRawResource(R.raw.english) + ?: throw FileNotFoundException(BIP39_ENGLISH_RESOURCE_NAME) + return stream + } + + private fun bytesToBits(data: ByteArray): BooleanArray { + val bits = BooleanArray(data.size * 8) + for (i in data.indices) + for (j in 0 until 8) + bits[i * 8 + j] = (data[i].toInt() and (1 shl (7 - j))) != 0 + return bits + } + + private fun ByteArray.toHexString(): String { + val sb = StringBuilder(size * 2) + for (b in this) { + val v = b.toInt() and 0xFF + sb.append("0123456789abcdef"[v ushr 4]) + sb.append("0123456789abcdef"[v and 0x0F]) + } + return sb.toString() + } + } +} diff --git a/libs/encryption/mnemonic/src/main/kotlin/com/getcode/crypt/MnemonicException.kt b/libs/encryption/mnemonic/src/main/kotlin/com/getcode/crypt/MnemonicException.kt new file mode 100644 index 0000000000..ff3e3ea98b --- /dev/null +++ b/libs/encryption/mnemonic/src/main/kotlin/com/getcode/crypt/MnemonicException.kt @@ -0,0 +1,33 @@ +package com.getcode.crypt + +/* + * Copyright 2013 Ken Sedgwick + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** Exceptions thrown by the [MnemonicCode] module. */ +sealed class MnemonicException(message: String? = null) : Exception(message) { + + /** Thrown when an argument to [MnemonicCode] is the wrong length. */ + class MnemonicLengthException(message: String) : MnemonicException(message) + + /** Thrown when a list of [MnemonicCode] words fails the checksum check. */ + class MnemonicChecksumException : MnemonicException() + + /** Thrown when a word is encountered which is not in the [MnemonicCode]'s word list. */ + class MnemonicWordException( + /** The word that was not found in the word list. */ + val badWord: String, + ) : MnemonicException() +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 2315b874f9..c95570e890 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -140,7 +140,7 @@ include( ":libs:emojis", ":libs:encryption:base58", ":libs:encryption:ed25519", - ":libs:encryption:ed25519-kmp", + ":libs:encryption:ed25519-native", ":libs:encryption:hmac", ":libs:encryption:keys", ":libs:encryption:mnemonic", @@ -253,7 +253,7 @@ val kmpUnitTestModules = setOf( ":libs:encryption:sha256", ":libs:encryption:sha512", ":libs:encryption:hmac", - ":libs:encryption:ed25519-kmp", + ":libs:encryption:ed25519", ) val noUnitTestModules = setOf(":apps:flipcash:benchmark", ":kmp:shared-core") val unitTestCandidates = includedProjectPaths.filter { path -> From 8ac507f06c669899074e765c296216d5f0ec5bcd Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 7 Aug 2026 17:33:25 -0400 Subject: [PATCH 4/7] fix(kmp/ed25519): re-export ed25519-native via api so consumers keep the JNI Ed25519 API --- libs/encryption/ed25519/build.gradle.kts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libs/encryption/ed25519/build.gradle.kts b/libs/encryption/ed25519/build.gradle.kts index 6b5a0d12b9..d724516e4e 100644 --- a/libs/encryption/ed25519/build.gradle.kts +++ b/libs/encryption/ed25519/build.gradle.kts @@ -107,8 +107,11 @@ kotlin { } androidMain { dependencies { - // Delegate to the JNI module for NDK/CMake compilation. - implementation(project(":libs:encryption:ed25519-native")) + // Delegate to the JNI module for NDK/CMake compilation. `api` (not + // `implementation`) so the JNI `com.getcode.ed25519.Ed25519` class is + // transitively re-exported to existing consumers of `:libs:encryption:ed25519` + // (e.g. :libs:encryption:utils) — keeps the rename consumer-transparent. + api(project(":libs:encryption:ed25519-native")) } } commonTest { From 8b73f7f9a105e968d69ebd5efaed61f25194e339 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 7 Aug 2026 17:36:08 -0400 Subject: [PATCH 5/7] ci: give compile-check a dummy local.properties so it can configure native modules --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca7b283a25..6542b991b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,17 @@ jobs: restore-keys: | gradle-build-cache- + # The build's convention plugins read keys from local.properties and fail if the + # file is absent. Compile-only needs no real secrets, so write a dummy file with + # sdk.dir + blank keys. (The flipcash-tests job gets this implicitly via its secret steps.) + - name: Create dummy local.properties (compile-only, no secrets) + run: | + echo "sdk.dir=$ANDROID_HOME" > local.properties + echo 'BUGSNAG_API_KEY=""' >> local.properties + echo 'GOOGLE_CLOUD_PROJECT_NUMBER=0' >> local.properties + echo 'MIXPANEL_API_KEY=""' >> local.properties + echo 'COINBASE_ONRAMP_API_KEY=' >> local.properties + # compileDebugSources runs all Kotlin/Java compilation tasks but stops before # processDebugGoogleServices (which needs secrets). This catches KMP interop # regressions (missing @JvmStatic, internal visibility leaking across modules) From c162f9a5f98a1d2d04923c87cb2951243883b956 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 10 Aug 2026 15:56:13 -0400 Subject: [PATCH 6/7] =?UTF-8?q?ci:=20green=20#1204=20=E2=80=94=20dummy=20g?= =?UTF-8?q?oogle-services.json=20for=20compile-check=20+=20exclude=20ed255?= =?UTF-8?q?19=20JNI=20host=20test=20on=20Linux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - compileDebugSources transitively needs processDebugGoogleServices; write a dummy google-services.json (pkg com.flipcash.app.android) so the credless gate can build. - ed25519's androidHostTest is a JNI vector test needing a host-native lib; it can't run on the Linux CI runner. Exclude it from CI unit tests (kmpUnitTestModules + noUnitTestModules); ed25519.json parity stays gated via the iOS cinterop path. --- .github/workflows/ci.yml | 15 +++++++++++---- settings.gradle.kts | 7 +++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6542b991b9..db5fd1193d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,10 +47,17 @@ jobs: echo 'MIXPANEL_API_KEY=""' >> local.properties echo 'COINBASE_ONRAMP_API_KEY=' >> local.properties - # compileDebugSources runs all Kotlin/Java compilation tasks but stops before - # processDebugGoogleServices (which needs secrets). This catches KMP interop - # regressions (missing @JvmStatic, internal visibility leaking across modules) - # that only surface when consumers try to compile against the shared library. + # compileDebugSources transitively runs processDebugGoogleServices (R generation + # consumes its generated resources), which needs a google-services.json. Compile-only + # needs no real Firebase project, so write a dummy matching the applicationId. + - name: Create dummy google-services.json (compile-only, no secrets) + run: | + mkdir -p apps/flipcash/app/src + echo '{"project_info":{"project_number":"000000000000","project_id":"flipcash-ci-dummy","storage_bucket":"flipcash-ci-dummy.appspot.com"},"client":[{"client_info":{"mobilesdk_app_id":"1:000000000000:android:0000000000000000000000","android_client_info":{"package_name":"com.flipcash.app.android"}},"oauth_client":[],"api_key":[{"current_key":"AIzaSyDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUM"}],"services":{"appinvite_service":{"other_platform_oauth_client":[]}}}],"configuration_version":"1"}' > apps/flipcash/app/src/google-services.json + + # This catches KMP interop regressions (missing @JvmStatic, internal visibility + # leaking across modules) that only surface when consumers compile against the + # shared library. processDebugGoogleServices runs but is satisfied by the dummy above. - name: Compile app sources run: ./gradlew :apps:flipcash:app:compileDebugSources --continue --no-daemon diff --git a/settings.gradle.kts b/settings.gradle.kts index 61bc8ea26b..8449301bf8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -253,10 +253,13 @@ val kmpUnitTestModules = setOf( ":libs:encryption:sha256", ":libs:encryption:sha512", ":libs:encryption:hmac", - ":libs:encryption:ed25519", + // ed25519's androidHostTest is a JNI vector test that needs a host-native lib + // (macOS .dylib / Linux .so); it can't load on the Linux CI runner. Its ed25519.json + // parity is gated via the iOS cinterop path (macOS) instead — so it's excluded here. ":libs:encryption:utils", ) -val noUnitTestModules = setOf(":apps:flipcash:benchmark", ":kmp:shared-core") +// ed25519 excluded: its only test is a JNI host vector test that can't run on Linux CI (see kmpUnitTestModules). +val noUnitTestModules = setOf(":apps:flipcash:benchmark", ":kmp:shared-core", ":libs:encryption:ed25519") val unitTestCandidates = includedProjectPaths.filter { path -> unitTestPaths.any { path == it || path.startsWith("$it:") } && path !in noUnitTestModules } From 42c0d7957f05ae320b7fa108e9e3ef623a092567 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 10 Aug 2026 16:18:08 -0400 Subject: [PATCH 7/7] ci: drop compile-check job; flipcash-tests already compiles everything --- .github/workflows/ci.yml | 50 ---------------------------------------- 1 file changed, 50 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db5fd1193d..5f80bfb141 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,56 +11,6 @@ env: JAVA_VERSION: 17 jobs: - compile-check: - name: Compile check (no creds required) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Setup Java env - uses: actions/setup-java@v3 - with: - java-version: '21' - distribution: 'corretto' - cache: 'gradle' - - - name: Gradle build cache - uses: actions/cache@v4 - with: - path: | - ~/.gradle/caches/build-cache-1 - .gradle/configuration-cache - key: gradle-build-cache-${{ hashFiles('**/*.gradle.kts', 'gradle.properties') }} - restore-keys: | - gradle-build-cache- - - # The build's convention plugins read keys from local.properties and fail if the - # file is absent. Compile-only needs no real secrets, so write a dummy file with - # sdk.dir + blank keys. (The flipcash-tests job gets this implicitly via its secret steps.) - - name: Create dummy local.properties (compile-only, no secrets) - run: | - echo "sdk.dir=$ANDROID_HOME" > local.properties - echo 'BUGSNAG_API_KEY=""' >> local.properties - echo 'GOOGLE_CLOUD_PROJECT_NUMBER=0' >> local.properties - echo 'MIXPANEL_API_KEY=""' >> local.properties - echo 'COINBASE_ONRAMP_API_KEY=' >> local.properties - - # compileDebugSources transitively runs processDebugGoogleServices (R generation - # consumes its generated resources), which needs a google-services.json. Compile-only - # needs no real Firebase project, so write a dummy matching the applicationId. - - name: Create dummy google-services.json (compile-only, no secrets) - run: | - mkdir -p apps/flipcash/app/src - echo '{"project_info":{"project_number":"000000000000","project_id":"flipcash-ci-dummy","storage_bucket":"flipcash-ci-dummy.appspot.com"},"client":[{"client_info":{"mobilesdk_app_id":"1:000000000000:android:0000000000000000000000","android_client_info":{"package_name":"com.flipcash.app.android"}},"oauth_client":[],"api_key":[{"current_key":"AIzaSyDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUM"}],"services":{"appinvite_service":{"other_platform_oauth_client":[]}}}],"configuration_version":"1"}' > apps/flipcash/app/src/google-services.json - - # This catches KMP interop regressions (missing @JvmStatic, internal visibility - # leaking across modules) that only surface when consumers compile against the - # shared library. processDebugGoogleServices runs but is satisfied by the dummy above. - - name: Compile app sources - run: ./gradlew :apps:flipcash:app:compileDebugSources --continue --no-daemon - flipcash-tests: name: Run Flipcash Tests runs-on: ubuntu-latest