ionspin / kotlin-multiplatform-libsodium

A kotlin multiplatform wrapper for libsodium, using directly built libsodium for jvm and native, and libsodium.js for js targets.
Apache License 2.0
89 stars 6 forks source link

KMP Testing can't load libdynamic-linux-arm64-libsodium.so in initialization #48

Open RoyalSWiSH opened 2 weeks ago

RoyalSWiSH commented 2 weeks ago

HI,

I try to initialize Libsodium from a test environment in a Kotlin Multiplattform project in the shared module from Android Studio.

import com.ionspin.kotlin.crypto.sample.EncryptionUtils
import com.ionspin.kotlin.crypto.secretbox.SecretBox
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.test.Test
import kotlin.test.assertContains
import kotlin.test.assertEquals
import com.ionspin.kotlin.crypto.LibsodiumInitializer
import kotlinx.coroutines.runBlocking

class EncryptionUtilsTest {

    @Test
    @OptIn(ExperimentalUnsignedTypes::class, ExperimentalEncodingApi::class)
    fun encryptMessageWithAssociatedDataTest() = runBlocking {
        LibsodiumInitializer.initializeWithCallback {
//                logger.info("Libsodium initialized")
        }
        val message = "Hello"
        val key = SecretBox.keygen()
        val encryptedMessage = EncryptionUtils.encryptMessageWithAssociatedData(message, key)
        val decryptedMessage = EncryptionUtils.decryptMessageWithAssociatedData(encryptedMessage, key)
        assertEquals(message, decryptedMessage)
    }

But it fails with this error

> Task :shared:testDebugUnitTest FAILED

java.lang.NullPointerException: Cannot invoke "java.net.URL.getFile()" because "url" is null
    at com.goterl.resourceloader.ResourceLoader.getFileFromFileSystem(ResourceLoader.java:244)
    at com.goterl.resourceloader.ResourceLoader.copyToTempDirectory(ResourceLoader.java:88)
    at com.goterl.resourceloader.SharedLibraryLoader.load(SharedLibraryLoader.java:53)
    at com.goterl.resourceloader.SharedLibraryLoader.load(SharedLibraryLoader.java:47)
    at com.ionspin.kotlin.crypto.LibsodiumInitializer.loadLibrary(LibsodiumInitializer.kt:19)
    at com.ionspin.kotlin.crypto.LibsodiumInitializer.initializeWithCallback(LibsodiumInitializer.kt:58)

My guess is, that it can't find the libsodium file in the testing environment here


    private fun loadLibrary() : JnaLibsodiumInterface {
        val libraryFile = when {
            Platform.isMac() -> {
                SharedLibraryLoader.get().load("libdynamic-macos.dylib", JnaLibsodiumInterface::class.java)
            }
            Platform.isLinux() -> {
                if (Platform.isARM()) {
                    SharedLibraryLoader.get().load("libdynamic-linux-arm64-libsodium.so", JnaLibsodiumInterface::class.java)
                } else {
                    SharedLibraryLoader.get()
                        .load("libdynamic-linux-x86-64-libsodium.so", JnaLibsodiumInterface::class.java)
                }
            }
            Platform.isWindows() -> {
                SharedLibraryLoader.get().load("libdynamic-msvc-x86-64-libsodium.dll", JnaLibsodiumInterface::class.java)
            }
            Platform.isAndroid() -> {
                File("irrelevant")
            }
            else -> throw RuntimeException("Unknown platform")

        }

This is the build.gradle.kts file from the shared module

import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink

plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidLibrary)
    id("org.jetbrains.kotlin.plugin.serialization") version "1.8.21"
    id("co.touchlab.skie") version "0.6.1"
}

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "1.8"
            }
        }
    }
    jvm {
        // Do not use withJava() for JVM targets in a multiplatform project with Android
    }

    val iosTargets = listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    )

    iosTargets.forEach {
        it.binaries.framework {
            baseName = "shared"
            isStatic = true
            freeCompilerArgs += listOf(
                "-Xoverride-konan-properties=osVersionMin.ios_arm32=16.1;osVersionMin.ios_arm64=16.1;osVersionMin.ios_x64=16.1",
                "-Xoverride-konan-properties=minVersion.ios=16.1;minVersionSinceXcode15.ios=16.1",
                "-Xbinary=bundleId=bio.gelly.proteinlookup"
            )
        }
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(libs.kotlinx.serialization.json)
                implementation("app.softwork:kotlinx-uuid-core:0.0.22")
                implementation(libs.ktor.client.core)
                implementation(libs.ktor.client.cio)
                implementation(libs.ktor.client.json)
                implementation(libs.ktor.client.content.negotiation)
                implementation(libs.ktor.serialization.kotlinx.json)
                implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.2")
            }
        }

        val iosMain by creating {
            dependencies {
                implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.2")
                implementation(libs.ktor.client.darwin)
            }
        }

        iosTargets.forEach {
            it.compilations["main"].defaultSourceSet {
                dependsOn(commonMain)
                dependsOn(iosMain)
            }
        }

        commonTest.dependencies {
            implementation(libs.kotlin.test)
            implementation(kotlin("test-common"))
            implementation(kotlin("test-annotations-common"))
            implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.2")
            implementation("org.slf4j:slf4j-simple:2.0.13") // or the latest version
            implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings:0.9.2")
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")

        }
        val jvmMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

               // Added for testing EncryptiionUtils which produces a SLF4J error otherwise
                implementation("org.slf4j:slf4j-simple:2.0.13") // or the latest version
            }
        }
        val jvmTest by getting {
            dependencies {
                implementation("org.jetbrains.kotlin:kotlin-test")
                implementation("org.jetbrains.kotlin:kotlin-test-junit")
                implementation("org.slf4j:slf4j-simple:2.0.13") // or the latest version
                implementation("com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings-jvm:0.9.2")

            }
        }

        // Ensure resources are included
        tasks.withType<Test> {
            systemProperty("java.library.path", "$buildDir/resources/main")
        }

    }

    // Workaround for compilation bug
//    tasks.register("testClasses")
}

android {
    namespace = "example.app.test"
    compileSdk = 34
    defaultConfig {
        minSdk = 29
    }

    sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
    sourceSets["main"].res.srcDirs("src/androidMain/res")
    sourceSets["main"].assets.srcDirs("src/androidMain/assets")
    sourceSets["main"].java.srcDirs("src/androidMain/kotlin")
    sourceSets["test"].java.srcDirs("src/androidTest/kotlin")
}
BernatCarbo commented 1 week ago

Same here!

ionspin commented 1 week ago

I think the reason is that unit test are run on x86 and libsodium doesnt have a build for android on x86 (last time I checked, which was some time ago). You should see similar behavior if you try to run the library on x86 android emulator.

Should be fixable by expanding libsoidum to build android x86 target, but I don't have bandwith to look into it right now. A workaround is to skip android unit tests.

-------- Original Message -------- On 6/24/24 10:56 PM, Bernat Carbó wrote:

Same here!

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you are subscribed to this thread.Message ID: @.***>

RoyalSWiSH commented 1 week ago

I run it on M3 Mac in Android Studio. Guess that is ARM. No issues in the simulator. I thought it was just because it can't access the .so file which is stored in the external library, when I perform local test in my app.

ionspin commented 1 week ago

Yes, I guess on arm mac it should work, and you should be able to get it to work by replacing file in this: ''' Platform.isAndroid() -> { File("irrelevant") } ''' with 'SharedLibraryLoader.get().load("libdynamic-macos.dylib", JnaLibsodiumInterface::class.java)'

I don't have access to a mac at the moment, so I can't test if that works correctly.

BernatCarbo commented 1 week ago

Interestingly, when using Windows, if I run a test from the common test source as Android(local) that calls LibsodiumInitializer.initialize(), it lands on Platform.windows so it tries to load libdynamic-msvc-x86-64-libsodium.dll and throws the same exception:

java.lang.NullPointerException: Cannot invoke "java.net.URL.getFile()" because "url" is null
    at com.goterl.resourceloader.ResourceLoader.getFileFromFileSystem(ResourceLoader.java:244)
    at com.goterl.resourceloader.ResourceLoader.copyToTempDirectory(ResourceLoader.java:88)
    at com.goterl.resourceloader.SharedLibraryLoader.load(SharedLibraryLoader.java:53)
    at com.goterl.resourceloader.SharedLibraryLoader.load(SharedLibraryLoader.java:47)
    at com.ionspin.kotlin.crypto.LibsodiumInitializer.loadLibrary(LibsodiumInitializer.kt:30)
    at com.ionspin.kotlin.crypto.LibsodiumInitializer.initialize(LibsodiumInitializer.kt:52)