diff --git a/build.gradle.kts b/build.gradle.kts index 60c9058..4c441fd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,15 +1,29 @@ plugins { kotlin("multiplatform") version "1.4-M2-mt" - id("com.android.library") version "4.0.0" - `maven-publish` +// `maven-publish` } -group = "fr.acinq.secp256k1" -version = "0.2.1-1.4-M2" -repositories { - jcenter() - google() - maven(url = "https://dl.bintray.com/kotlin/kotlin-eap") +buildscript { + repositories { + google() + maven("https://dl.bintray.com/kotlin/kotlin-eap") + jcenter() + } + + dependencies { + classpath("com.android.tools.build:gradle:4.0.0") + } +} + +allprojects { + group = "fr.acinq.secp256k1" + version = "0.2.1-1.4-M2" + + repositories { + jcenter() + google() + maven(url = "https://dl.bintray.com/kotlin/kotlin-eap") + } } val currentOs = org.gradle.internal.os.OperatingSystem.current() @@ -29,60 +43,40 @@ kotlin { } } - val jvmAndAndroidMain by sourceSets.creating { - dependsOn(commonMain) - dependencies { - implementation(kotlin("stdlib-jdk8")) - } - } - jvm { compilations.all { kotlinOptions.jvmTarget = "1.8" } - (tasks[compilations["main"].processResourcesTaskName] as ProcessResources).apply{ - dependsOn("copyJni") - from(buildDir.resolve("jniResources")) + compilations["main"].dependencies { + implementation(kotlin("stdlib-jdk8")) } - compilations["main"].defaultSourceSet.dependsOn(jvmAndAndroidMain) compilations["test"].dependencies { + implementation(project(":jni")) + implementation(kotlin("stdlib-jdk8")) implementation(kotlin("test-junit")) } } - android { - publishLibraryVariants("release", "debug") - compilations.all { - kotlinOptions.jvmTarget = "1.8" - } - sourceSets["androidMain"].dependsOn(jvmAndAndroidMain) - sourceSets["androidTest"].dependencies { - implementation(kotlin("test-junit")) - implementation("androidx.test.ext:junit:1.1.1") - implementation("androidx.test.espresso:espresso-core:3.2.0") - } - } - - fun org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget.secp256k1CInterop() { + fun org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget.secp256k1CInterop(target: String) { compilations["main"].cinterops { val libsecp256k1 by creating { includeDirs.headerFilterOnly(project.file("native/secp256k1/include/")) - tasks[interopProcessingTaskName].dependsOn("buildSecp256k1Ios") + tasks[interopProcessingTaskName].dependsOn(":native:buildSecp256k1${target.capitalize()}") } } } val nativeMain by sourceSets.creating { dependsOn(commonMain) } - linuxX64 { - secp256k1CInterop() + linuxX64("linux") { + secp256k1CInterop("linux") // https://youtrack.jetbrains.com/issue/KT-39396 compilations["main"].kotlinOptions.freeCompilerArgs += listOf("-include-binary", "$rootDir/native/build/linux/libsecp256k1.a") compilations["main"].defaultSourceSet.dependsOn(nativeMain) } ios { - secp256k1CInterop() + secp256k1CInterop("ios") // https://youtrack.jetbrains.com/issue/KT-39396 compilations["main"].kotlinOptions.freeCompilerArgs += listOf("-include-binary", "$rootDir/native/build/ios/libsecp256k1.a") compilations["main"].defaultSourceSet.dependsOn(nativeMain) @@ -120,166 +114,43 @@ afterEvaluate { } } -android { - defaultConfig { - compileSdkVersion(30) - minSdkVersion(21) - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - externalNativeBuild { - cmake {} - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - externalNativeBuild { - cmake { - setPath("src/androidMain/CMakeLists.txt") - } - } - ndkVersion = "21.3.6528147" - - sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") - - afterEvaluate { - tasks.withType().all { - enabled = false - } - } -} - -val buildSecp256k1 by tasks.creating { group = "build" } -sealed class Cross { - abstract fun cmd(target: String, nativeDir: File): List - class DockCross(val cross: String) : Cross() { - override fun cmd(target: String, nativeDir: File): List = listOf("./dockcross-$cross", "bash", "-c", "CROSS=1 TARGET=$target ./build.sh") - } - class MultiArch(val crossTriple: String) : Cross() { - override fun cmd(target: String, nativeDir: File): List { - val uid = Runtime.getRuntime().exec("id -u").inputStream.use { it.reader().readText() }.trim().toInt() - return listOf( - "docker", "run", "--rm", "-v", "${nativeDir.absolutePath}:/workdir", - "-e", "CROSS_TRIPLE=$crossTriple", "-e", "TARGET=$target", "-e", "TO_UID=$uid", "-e", "CROSS=1", - "multiarch/crossbuild", "./build.sh" - ) - } - } -} -fun creatingBuildSecp256k1(target: String, cross: Cross?) = tasks.creating(Exec::class) { - group = "build" - buildSecp256k1.dependsOn(this) - - inputs.files(projectDir.resolve("native/build.sh")) - outputs.dir(projectDir.resolve("native/build/$target")) - - workingDir = projectDir.resolve("native") - environment("TARGET", target) - commandLine((cross?.cmd(target, workingDir) ?: emptyList()) + "./build.sh") -} -val buildSecp256k1Darwin by creatingBuildSecp256k1("darwin", if (currentOs.isMacOsX) null else Cross.MultiArch("x86_64-apple-darwin")) -val buildSecp256k1Linux by creatingBuildSecp256k1("linux", if (currentOs.isLinux) null else Cross.DockCross("linux-x64")) -val buildSecp256k1Mingw by creatingBuildSecp256k1("mingw", if (currentOs.isWindows) null else Cross.DockCross("windows-x64")) - -val copyJni by tasks.creating(Sync::class) { - dependsOn(buildSecp256k1) - from(projectDir.resolve("native/build/linux/libsecp256k1-jni.so")) { rename { "libsecp256k1-jni-linux-x86_64.so" } } - from(projectDir.resolve("native/build/darwin/libsecp256k1-jni.dylib")) { rename { "libsecp256k1-jni-darwin-x86_64.dylib" } } - from(projectDir.resolve("native/build/mingw/secp256k1-jni.dll")) { rename { "secp256k1-jni-mingw-x86_64.dll" } } - into(buildDir.resolve("jniResources/fr/acinq/secp256k1/native")) -} - -val buildSecp256k1Ios by tasks.creating(Exec::class) { - group = "build" - buildSecp256k1.dependsOn(this) - - onlyIf { currentOs.isMacOsX } - - inputs.files(projectDir.resolve("native/build-ios.sh")) - outputs.dir(projectDir.resolve("native/build/ios")) - - workingDir = projectDir.resolve("native") - commandLine("./build-ios.sh") -} - -val buildSecp256k1Android by tasks.creating { - group = "build" - buildSecp256k1.dependsOn(this) -} -fun creatingBuildSecp256k1Android(arch: String) = tasks.creating(Exec::class) { - group = "build" - buildSecp256k1Android.dependsOn(this) - - inputs.files(projectDir.resolve("native/build-android.sh")) - outputs.dir(projectDir.resolve("native/build/android/$arch")) - - workingDir = projectDir.resolve("native") - - val toolchain = when { - currentOs.isLinux -> "linux-x86_64" - currentOs.isMacOsX -> "darwin-x86_64" - currentOs.isWindows -> "windows-x86_64" - else -> error("No Android toolchain defined for this OS: $currentOs") - } - environment("TOOLCHAIN", toolchain) - environment("ARCH", arch) - environment("ANDROID_NDK", android.ndkDirectory) - commandLine("./build-android.sh") -} -val buildSecp256k1AndroidX86_64 by creatingBuildSecp256k1Android("x86_64") -val buildSecp256k1AndroidX86 by creatingBuildSecp256k1Android("x86") -val buildSecp256k1AndroidArm64v8a by creatingBuildSecp256k1Android("arm64-v8a") -val buildSecp256k1AndroidArmeabiv7a by creatingBuildSecp256k1Android("armeabi-v7a") - -afterEvaluate { - configure(listOf("Debug", "Release").map { tasks["externalNativeBuild$it"] }) { - dependsOn(buildSecp256k1Android) - } -} - -tasks["clean"].doLast { - delete(projectDir.resolve("native/build")) -} - -publishing { - val snapshotName: String? by project - val snapshotNumber: String? by project - - val bintrayUsername: String? = (properties["bintrayUsername"] as String?) ?: System.getenv("BINTRAY_USER") - val bintrayApiKey: String? = (properties["bintrayApiKey"] as String?) ?: System.getenv("BINTRAY_APIKEY") - if (bintrayUsername == null || bintrayApiKey == null) logger.warn("Skipping bintray configuration as bintrayUsername or bintrayApiKey is not defined") - else { - val btRepo = if (snapshotNumber != null) "snapshots" else "libs" - repositories { - maven { - name = "bintray" - setUrl("https://api.bintray.com/maven/acinq/$btRepo/${project.name}/;publish=0") - credentials { - username = bintrayUsername - password = bintrayApiKey - } - } - } - } - - publications.withType().configureEach { - if (snapshotName != null && snapshotNumber != null) version = "${project.version}-${snapshotName}-${snapshotNumber}" - pom { - description.set("Bitcoin's secp256k1 library ported to Kotlin/Multiplatform for JVM, Android, iOS & Linux") - url.set("https://github.com/ACINQ/secp256k1-kmp") - licenses { - name.set("Apache License v2.0") - url.set("https://www.apache.org/licenses/LICENSE-2.0") - } - issueManagement { - system.set("Github") - url.set("https://github.com/ACINQ/secp256k1-kmp/issues") - } - scm { - connection.set("https://github.com/ACINQ/secp256k1-kmp.git") - } - } - } -} +//publishing { +// val snapshotName: String? by project +// val snapshotNumber: String? by project +// +// val bintrayUsername: String? = (properties["bintrayUsername"] as String?) ?: System.getenv("BINTRAY_USER") +// val bintrayApiKey: String? = (properties["bintrayApiKey"] as String?) ?: System.getenv("BINTRAY_APIKEY") +// if (bintrayUsername == null || bintrayApiKey == null) logger.warn("Skipping bintray configuration as bintrayUsername or bintrayApiKey is not defined") +// else { +// val btRepo = if (snapshotNumber != null) "snapshots" else "libs" +// repositories { +// maven { +// name = "bintray" +// setUrl("https://api.bintray.com/maven/acinq/$btRepo/${project.name}/;publish=0") +// credentials { +// username = bintrayUsername +// password = bintrayApiKey +// } +// } +// } +// } +// +// publications.withType().configureEach { +// if (snapshotName != null && snapshotNumber != null) version = "${project.version}-${snapshotName}-${snapshotNumber}" +// pom { +// description.set("Bitcoin's secp256k1 library ported to Kotlin/Multiplatform for JVM, Android, iOS & Linux") +// url.set("https://github.com/ACINQ/secp256k1-kmp") +// licenses { +// name.set("Apache License v2.0") +// url.set("https://www.apache.org/licenses/LICENSE-2.0") +// } +// issueManagement { +// system.set("Github") +// url.set("https://github.com/ACINQ/secp256k1-kmp/issues") +// } +// scm { +// connection.set("https://github.com/ACINQ/secp256k1-kmp.git") +// } +// } +// } +//} diff --git a/jni/build.gradle.kts b/jni/build.gradle.kts new file mode 100644 index 0000000..d3faf6e --- /dev/null +++ b/jni/build.gradle.kts @@ -0,0 +1,103 @@ +plugins { + kotlin("multiplatform") // version "1.4-M2-mt" + id("com.android.library") + `maven-publish` +} + +val currentOs = org.gradle.internal.os.OperatingSystem.current() + +kotlin { + explicitApi() + + val commonMain by sourceSets.getting { + dependencies { + implementation(kotlin("stdlib-common")) + } + } + val commonTest by sourceSets.getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + + jvm { + compilations.all { + kotlinOptions.jvmTarget = "1.8" + } + (tasks[compilations["main"].processResourcesTaskName] as ProcessResources).apply { + dependsOn("copyJni") + from(buildDir.resolve("jniResources")) + } + compilations["main"].dependencies { + implementation(kotlin("stdlib-jdk8")) + } + compilations["test"].dependencies { + implementation(kotlin("test-junit")) + } + } + + android { + publishLibraryVariants("release", "debug") + compilations.all { + kotlinOptions.jvmTarget = "1.8" + } + sourceSets["androidMain"].dependencies { + implementation(kotlin("stdlib-jdk8")) + } + sourceSets["androidTest"].dependencies { + implementation(kotlin("test-junit")) + implementation("androidx.test.ext:junit:1.1.1") + implementation("androidx.test.espresso:espresso-core:3.2.0") + } + } + + sourceSets.all { + languageSettings.useExperimentalAnnotation("kotlin.RequiresOptIn") + } +} + +android { + defaultConfig { + compileSdkVersion(30) + minSdkVersion(21) + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake {} + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + externalNativeBuild { + cmake { + setPath("src/androidMain/CMakeLists.txt") + } + } + ndkVersion = "21.3.6528147" + + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + + afterEvaluate { + tasks.withType().all { + enabled = false + } + } +} + +val copyJni by tasks.creating(Sync::class) { + dependsOn(":native:buildSecp256k1Jvm") + from(rootDir.resolve("native/build/linux/libsecp256k1-jni.so")) { rename { "libsecp256k1-jni-linux-x86_64.so" } } + from(rootDir.resolve("native/build/darwin/libsecp256k1-jni.dylib")) { rename { "libsecp256k1-jni-darwin-x86_64.dylib" } } + from(rootDir.resolve("native/build/mingw/secp256k1-jni.dll")) { rename { "secp256k1-jni-mingw-x86_64.dll" } } + into(buildDir.resolve("jniResources/fr/acinq/secp256k1/jni/native")) +} + +afterEvaluate { + configure(listOf("Debug", "Release").map { tasks["externalNativeBuild$it"] }) { + dependsOn(":native:buildSecp256k1Android") + } +} diff --git a/src/androidMain/AndroidManifest.xml b/jni/src/androidMain/AndroidManifest.xml similarity index 100% rename from src/androidMain/AndroidManifest.xml rename to jni/src/androidMain/AndroidManifest.xml diff --git a/jni/src/androidMain/CMakeLists.txt b/jni/src/androidMain/CMakeLists.txt new file mode 100644 index 0000000..553d148 --- /dev/null +++ b/jni/src/androidMain/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.10.0) + +add_library( secp256k1-jni SHARED + ${CMAKE_CURRENT_LIST_DIR}/../../../native/jni/src/org_bitcoin_NativeSecp256k1.c + ${CMAKE_CURRENT_LIST_DIR}/../../../native/jni/src/org_bitcoin_Secp256k1Context.c +) + +target_include_directories( secp256k1-jni + PUBLIC ${CMAKE_CURRENT_LIST_DIR}/../../../native/secp256k1 +) + +target_link_libraries( secp256k1-jni + ${CMAKE_CURRENT_LIST_DIR}/../../../native/build/android/${ANDROID_ABI}/libsecp256k1.a +) diff --git a/jni/src/androidMain/kotlin/fr/acinq/secp256k1/jni/NativeSecp256k1Loader.kt b/jni/src/androidMain/kotlin/fr/acinq/secp256k1/jni/NativeSecp256k1Loader.kt new file mode 100644 index 0000000..b1b2584 --- /dev/null +++ b/jni/src/androidMain/kotlin/fr/acinq/secp256k1/jni/NativeSecp256k1Loader.kt @@ -0,0 +1,12 @@ +package fr.acinq.secp256k1.jni + +public actual object NativeSecp256k1Loader { + + @JvmStatic + @Synchronized + @Throws(Exception::class) + actual fun load() { + System.loadLibrary("secp256k1-jni") + } + +} diff --git a/jni/src/commonMain/kotlin/fr/acinq/secp256k1/jni/NativeSecp256k1Loader.kt b/jni/src/commonMain/kotlin/fr/acinq/secp256k1/jni/NativeSecp256k1Loader.kt new file mode 100644 index 0000000..a50fc0d --- /dev/null +++ b/jni/src/commonMain/kotlin/fr/acinq/secp256k1/jni/NativeSecp256k1Loader.kt @@ -0,0 +1,10 @@ +package fr.acinq.secp256k1.jni + +import kotlin.jvm.JvmStatic + + +public expect object NativeSecp256k1Loader { + + public fun load() + +} diff --git a/src/jvmAndAndroidMain/kotlin/org/bitcoin/NativeSecp256k1.kt b/jni/src/commonMain/kotlin/org/bitcoin/NativeSecp256k1.kt similarity index 100% rename from src/jvmAndAndroidMain/kotlin/org/bitcoin/NativeSecp256k1.kt rename to jni/src/commonMain/kotlin/org/bitcoin/NativeSecp256k1.kt diff --git a/src/jvmAndAndroidMain/kotlin/org/bitcoin/NativeSecp256k1Util.kt b/jni/src/commonMain/kotlin/org/bitcoin/NativeSecp256k1Util.kt similarity index 100% rename from src/jvmAndAndroidMain/kotlin/org/bitcoin/NativeSecp256k1Util.kt rename to jni/src/commonMain/kotlin/org/bitcoin/NativeSecp256k1Util.kt diff --git a/src/jvmAndAndroidMain/kotlin/org/bitcoin/Secp256k1Context.kt b/jni/src/commonMain/kotlin/org/bitcoin/Secp256k1Context.kt similarity index 95% rename from src/jvmAndAndroidMain/kotlin/org/bitcoin/Secp256k1Context.kt rename to jni/src/commonMain/kotlin/org/bitcoin/Secp256k1Context.kt index 1d4731c..0cbb769 100644 --- a/src/jvmAndAndroidMain/kotlin/org/bitcoin/Secp256k1Context.kt +++ b/jni/src/commonMain/kotlin/org/bitcoin/Secp256k1Context.kt @@ -15,7 +15,7 @@ */ package org.bitcoin -import fr.acinq.secp256k1.Secp256k1Loader.initialize +import kotlin.jvm.JvmStatic /** * This class holds the context reference used in native methods diff --git a/jni/src/jvmMain/kotlin/fr/acinq/secp256k1/jni/NativeSecp256k1Loader.kt b/jni/src/jvmMain/kotlin/fr/acinq/secp256k1/jni/NativeSecp256k1Loader.kt new file mode 100644 index 0000000..9678781 --- /dev/null +++ b/jni/src/jvmMain/kotlin/fr/acinq/secp256k1/jni/NativeSecp256k1Loader.kt @@ -0,0 +1,211 @@ +package fr.acinq.secp256k1.jni + +import java.io.* +import java.util.* + +/** +* Set the system properties, org.sqlite.lib.path, org.sqlite.lib.name, +* appropriately so that the SQLite JDBC driver can find *.dll, *.jnilib and +* *.so files, according to the current OS (win, linux, mac). +* The library files are automatically extracted from this project's package (JAR). +* usage: call [.initialize] before using SQLite JDBC driver. +* +* @author leo +*/ +public actual object NativeSecp256k1Loader { + private var extracted = false + + /** + * Loads secp256k1 native library. + * + * @return True if secp256k1 native library is successfully loaded; false otherwise. + * @throws Exception if loading fails + */ + @JvmStatic + @Synchronized + @Throws(Exception::class) + public actual fun load() { + // only cleanup before the first extract + if (!extracted) { + cleanup() + } + loadSecp256k1NativeLibrary() + } + + private val tempDir: File + get() = File( + System.getProperty( + "fr.acinq.secp256k1.tmpdir", + System.getProperty("java.io.tmpdir") + ) + ) + + /** + * Deleted old native libraries e.g. on Windows the DLL file is not removed + * on VM-Exit (bug #80) + */ + @JvmStatic + public fun cleanup() { + val tempFolder = tempDir.absolutePath + val dir = File(tempFolder) + val nativeLibFiles = dir.listFiles(object : FilenameFilter { + private val searchPattern = "secp256k1-" + override fun accept(dir: File, name: String): Boolean { + return name.startsWith(searchPattern) && !name.endsWith(".lck") + } + }) + if (nativeLibFiles != null) { + for (nativeLibFile in nativeLibFiles) { + val lckFile = File(nativeLibFile.absolutePath + ".lck") + if (!lckFile.exists()) { + try { + nativeLibFile.delete() + } catch (e: SecurityException) { + System.err.println("Failed to delete old native lib" + e.message) + } + } + } + } + } + + @Throws(IOException::class) + private fun InputStream.contentsEquals(other: InputStream): Boolean { + val bufThis = this as? BufferedInputStream + ?: BufferedInputStream(this) + val bufOther = other as? BufferedInputStream + ?: BufferedInputStream(other) + var ch = bufThis.read() + while (ch != -1) { + val ch2 = bufOther.read() + if (ch != ch2) return false + ch = bufThis.read() + } + val ch2 = bufOther.read() + return ch2 == -1 + } + + /** + * Extracts and loads the specified library file to the target folder + * + * @param libDir Library path. + * @param libFileName Library name. + * @param targetDirectory Target folder. + * @return + */ + private fun extractAndLoadLibraryFile(libDir: String, libFileName: String, targetDirectory: String): Boolean { + val libPath = "$libDir/$libFileName" + // Include architecture name in temporary filename in order to avoid conflicts + // when multiple JVMs with different architectures running at the same time + val uuid = UUID.randomUUID().toString() + val extractedLibFileName = String.format("secp256k1-%s-%s", uuid, libFileName) + val extractedLckFileName = "$extractedLibFileName.lck" + val extractedLibFile = File(targetDirectory, extractedLibFileName) + val extractedLckFile = File(targetDirectory, extractedLckFileName) + return try { + // Extract a native library file into the target directory + val reader = NativeSecp256k1Loader::class.java.getResourceAsStream(libPath) + if (!extractedLckFile.exists()) { + FileOutputStream(extractedLckFile).close() + } + val writer = FileOutputStream(extractedLibFile) + try { + val buffer = ByteArray(8192) + var bytesRead = reader.read(buffer) + while (bytesRead != -1) { + writer.write(buffer, 0, bytesRead) + bytesRead = reader.read(buffer) + } + } finally { + // Delete the extracted lib file on JVM exit. + extractedLibFile.deleteOnExit() + extractedLckFile.deleteOnExit() + writer.close() + reader.close() + } + + // Set executable (x) flag to enable Java to load the native library + extractedLibFile.setReadable(true) + extractedLibFile.setWritable(true, true) + extractedLibFile.setExecutable(true) + + // Check whether the contents are properly copied from the resource folder + NativeSecp256k1Loader::class.java.getResourceAsStream(libPath).use { nativeIn -> + FileInputStream(extractedLibFile).use { extractedLibIn -> + if (!nativeIn.contentsEquals(extractedLibIn)) { + throw RuntimeException( + String.format( + "Failed to write a native library file at %s", + extractedLibFile + ) + ) + } + } + } + + loadNativeLibrary(targetDirectory, extractedLibFileName) + } catch (e: IOException) { + System.err.println(e.message) + false + } + } + + /** + * Loads native library using the given path and name of the library. + * + * @param path Path of the native library. + * @param name Name of the native library. + * @return True for successfully loading; false otherwise. + */ + private fun loadNativeLibrary(path: String, name: String): Boolean { + val libPath = File(path, name) + return if (libPath.exists()) { + try { + System.load(File(path, name).absolutePath) + true + } catch (e: UnsatisfiedLinkError) { + System.err.println("Failed to load native library:$name. osinfo: ${OSInfo.nativeSuffix}") + System.err.println(e) + false + } + } else { + false + } + } + + /** + * Loads secp256k1 native library using given path and name of the library. + * + * @throws + */ + private fun loadSecp256k1NativeLibrary() { + if (extracted) { + return + } + + // Try loading library from fr.acinq.secp256k1.lib.path library path */ + val libraryPath = System.getProperty("fr.acinq.secp256k1.lib.path") + val libraryName = System.getProperty("fr.acinq.secp256k1.lib.name") ?: System.mapLibraryName("secp256k1-jni-${OSInfo.nativeSuffix}") + if (libraryPath != null) { + if (loadNativeLibrary(libraryPath, libraryName)) { + extracted = true + return + } + } + + // Load the os-dependent library from the jar file + val packagePath = NativeSecp256k1Loader::class.java.getPackage().name.replace("\\.".toRegex(), "/") + val embeddedLibraryPath = "/$packagePath/native" + val hasNativeLib = NativeSecp256k1Loader::class.java.getResource("$embeddedLibraryPath/$libraryName") != null + if (!hasNativeLib) { + error("No native library found: at $embeddedLibraryPath/$libraryName") + } + + // Try extracting the library from jar + if (extractAndLoadLibraryFile(embeddedLibraryPath, libraryName, tempDir.absolutePath)) { + extracted = true + return + } + extracted = false + return + } +} \ No newline at end of file diff --git a/jni/src/jvmMain/kotlin/fr/acinq/secp256k1/jni/OSInfo.kt b/jni/src/jvmMain/kotlin/fr/acinq/secp256k1/jni/OSInfo.kt new file mode 100644 index 0000000..d120542 --- /dev/null +++ b/jni/src/jvmMain/kotlin/fr/acinq/secp256k1/jni/OSInfo.kt @@ -0,0 +1,214 @@ +package fr.acinq.secp256k1.jni + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.* + +/** +* Provides OS name and architecture name. +* +* @author leo +*/ +@Suppress("DuplicatedCode") +internal object OSInfo { + private val archMapping = HashMap() + private const val X86 = "x86" + private const val X86_64 = "x86_64" + private const val IA64_32 = "ia64_32" + private const val IA64 = "ia64" + private const val PPC = "ppc" + private const val PPC64 = "ppc64" + + @JvmStatic val nativeSuffix: String get() = "$os-$arch" + + @JvmStatic val os: String get() = translateOSName(System.getProperty("os.name")) + + @JvmStatic val hardwareName: String get() = + try { + val p = Runtime.getRuntime().exec("uname -m") + p.waitFor() + val input = p.inputStream + input.use { + val b = ByteArrayOutputStream() + val buf = ByteArray(32) + var readLen = it.read(buf, 0, buf.size) + while (readLen >= 0) { + b.write(buf, 0, readLen) + readLen = it.read(buf, 0, buf.size) + } + b.toString() + } + } catch (e: Throwable) { + System.err.println("Error while running uname -m: " + e.message) + "unknown" + } + + @JvmStatic + private fun resolveArmArchType(): String { + if (System.getProperty("os.name").contains("Linux")) { + val armType = hardwareName + // armType (uname -m) can be armv5t, armv5te, armv5tej, armv5tejl, armv6, armv7, armv7l, aarch64, i686// ignored: fall back to "arm" arch (soft-float ABI) + // ignored: fall back to "arm" arch (soft-float ABI) + // determine if first JVM found uses ARM hard-float ABI + when { + armType.startsWith("armv6") -> { + // Raspberry PI + return "armv6" + } + armType.startsWith("armv7") -> { + // Generic + return "armv7" + } + armType.startsWith("armv5") -> { + // Use armv5, soft-float ABI + return "arm" + } + armType == "aarch64" -> { + // Use arm64 + return "arm64" + } + + // Java 1.8 introduces a system property to determine armel or armhf + // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8005545 + + // For java7, we stil need to if run some shell commands to determine ABI of JVM + else -> { + val abi = System.getProperty("sun.arch.abi") + if (abi != null && abi.startsWith("gnueabihf")) { + return "armv7" + } + + // For java7, we stil need to if run some shell commands to determine ABI of JVM + val javaHome = System.getProperty("java.home") + try { + // determine if first JVM found uses ARM hard-float ABI + var exitCode = Runtime.getRuntime().exec("which readelf").waitFor() + if (exitCode == 0) { + val cmdarray = arrayOf( + "/bin/sh", "-c", "find '" + javaHome + + "' -name 'libjvm.so' | head -1 | xargs readelf -A | " + + "grep 'Tag_ABI_VFP_args: VFP registers'" + ) + exitCode = Runtime.getRuntime().exec(cmdarray).waitFor() + if (exitCode == 0) { + return "armv7" + } + } else { + System.err.println( + "WARNING! readelf not found. Cannot check if running on an armhf system, " + + "armel architecture will be presumed." + ) + } + } catch (e: IOException) { + // ignored: fall back to "arm" arch (soft-float ABI) + } catch (e: InterruptedException) { + // ignored: fall back to "arm" arch (soft-float ABI) + } + } + } + + // Java 1.8 introduces a system property to determine armel or armhf + // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8005545 + val abi = System.getProperty("sun.arch.abi") + if (abi != null && abi.startsWith("gnueabihf")) { + return "armv7" + } + + // For java7, we stil need to if run some shell commands to determine ABI of JVM + val javaHome = System.getProperty("java.home") + try { + // determine if first JVM found uses ARM hard-float ABI + var exitCode = Runtime.getRuntime().exec("which readelf").waitFor() + if (exitCode == 0) { + val cmdarray = arrayOf( + "/bin/sh", "-c", "find '" + javaHome + + "' -name 'libjvm.so' | head -1 | xargs readelf -A | " + + "grep 'Tag_ABI_VFP_args: VFP registers'" + ) + exitCode = Runtime.getRuntime().exec(cmdarray).waitFor() + if (exitCode == 0) { + return "armv7" + } + } else { + System.err.println( + "WARNING! readelf not found. Cannot check if running on an armhf system, " + + "armel architecture will be presumed." + ) + } + } catch (e: IOException) { + // ignored: fall back to "arm" arch (soft-float ABI) + } catch (e: InterruptedException) { + // ignored: fall back to "arm" arch (soft-float ABI) + } + } + // Use armv5, soft-float ABI + return "arm" + } + + // For Android + @JvmStatic + val arch: String? + get() { + val systemOsArch = System.getProperty("os.arch") + val osArch = + if (systemOsArch.startsWith("arm")) { + resolveArmArchType() + } else { + val lc = systemOsArch.toLowerCase(Locale.US) + if (archMapping.containsKey(lc)) return archMapping[lc] + systemOsArch + } + return translateArchNameToFolderName(osArch) + } + + @JvmStatic + fun translateOSName(osName: String): String = + when { + osName.contains("Windows") -> "mingw" + osName.contains("Mac") || osName.contains("Darwin") -> "darwin" + osName.contains("Linux") -> "linux" + osName.contains("AIX") -> "aix" + else -> osName.replace("\\W".toRegex(), "") + } + + @JvmStatic + fun translateArchNameToFolderName(archName: String): String = archName.replace("\\W".toRegex(), "") + + init { + // x86 mappings + archMapping[X86] = X86 + archMapping["i386"] = X86 + archMapping["i486"] = X86 + archMapping["i586"] = X86 + archMapping["i686"] = X86 + archMapping["pentium"] = X86 + + // x86_64 mappings + archMapping[X86_64] = X86_64 + archMapping["amd64"] = X86_64 + archMapping["em64t"] = X86_64 + archMapping["universal"] = X86_64 // Needed for openjdk7 in Mac + + // Itenium 64-bit mappings + archMapping[IA64] = IA64 + archMapping["ia64w"] = IA64 + + // Itenium 32-bit mappings, usually an HP-UX construct + archMapping[IA64_32] = IA64_32 + archMapping["ia64n"] = IA64_32 + + // PowerPC mappings + archMapping[PPC] = PPC + archMapping["power"] = PPC + archMapping["powerpc"] = PPC + archMapping["power_pc"] = PPC + archMapping["power_rs"] = PPC + + // TODO: PowerPC 64bit mappings + archMapping[PPC64] = PPC64 + archMapping["power64"] = PPC64 + archMapping["powerpc64"] = PPC64 + archMapping["power_pc64"] = PPC64 + archMapping["power_rs64"] = PPC64 + } +} \ No newline at end of file diff --git a/native/build.gradle.kts b/native/build.gradle.kts new file mode 100644 index 0000000..e65e1e9 --- /dev/null +++ b/native/build.gradle.kts @@ -0,0 +1,88 @@ +evaluationDependsOn(":jni") + +val currentOs = org.gradle.internal.os.OperatingSystem.current() + +val buildSecp256k1 by tasks.creating { group = "build" } + +sealed class Cross { + abstract fun cmd(target: String, nativeDir: File): List + class DockCross(val cross: String) : Cross() { + override fun cmd(target: String, nativeDir: File): List = listOf("./dockcross-$cross", "bash", "-c", "CROSS=1 TARGET=$target ./build.sh") + } + class MultiArch(val crossTriple: String) : Cross() { + override fun cmd(target: String, nativeDir: File): List { + val uid = Runtime.getRuntime().exec("id -u").inputStream.use { it.reader().readText() }.trim().toInt() + return listOf( + "docker", "run", "--rm", "-v", "${nativeDir.absolutePath}:/workdir", + "-e", "CROSS_TRIPLE=$crossTriple", "-e", "TARGET=$target", "-e", "TO_UID=$uid", "-e", "CROSS=1", + "multiarch/crossbuild", "./build.sh" + ) + } + } +} +val buildSecp256k1Jvm by tasks.creating { + group = "build" + buildSecp256k1.dependsOn(this) +} +fun creatingBuildSecp256k1(target: String, cross: Cross?) = tasks.creating(Exec::class) { + group = "build" + buildSecp256k1Jvm.dependsOn(this) + + inputs.files(projectDir.resolve("build.sh")) + outputs.dir(projectDir.resolve("build/$target")) + + workingDir = projectDir + environment("TARGET", target) + commandLine((cross?.cmd(target, workingDir) ?: emptyList()) + "./build.sh") +} +val buildSecp256k1Darwin by creatingBuildSecp256k1("darwin", if (currentOs.isMacOsX) null else Cross.MultiArch("x86_64-apple-darwin")) +val buildSecp256k1Linux by creatingBuildSecp256k1("linux", if (currentOs.isLinux) null else Cross.DockCross("linux-x64")) +val buildSecp256k1Mingw by creatingBuildSecp256k1("mingw", if (currentOs.isWindows) null else Cross.DockCross("windows-x64")) + +val buildSecp256k1Ios by tasks.creating(Exec::class) { + group = "build" + buildSecp256k1.dependsOn(this) + + onlyIf { currentOs.isMacOsX } + + inputs.files(projectDir.resolve("build-ios.sh")) + outputs.dir(projectDir.resolve("build/ios")) + + workingDir = projectDir + commandLine("./build-ios.sh") +} + +val buildSecp256k1Android by tasks.creating { + group = "build" + buildSecp256k1.dependsOn(this) +} +fun creatingBuildSecp256k1Android(arch: String) = tasks.creating(Exec::class) { + group = "build" + buildSecp256k1Android.dependsOn(this) + + inputs.files(projectDir.resolve("build-android.sh")) + outputs.dir(projectDir.resolve("build/android/$arch")) + + workingDir = projectDir + + val toolchain = when { + currentOs.isLinux -> "linux-x86_64" + currentOs.isMacOsX -> "darwin-x86_64" + currentOs.isWindows -> "windows-x86_64" + else -> error("No Android toolchain defined for this OS: $currentOs") + } + environment("TOOLCHAIN", toolchain) + environment("ARCH", arch) + environment("ANDROID_NDK", (project(":jni").extensions["android"] as com.android.build.gradle.LibraryExtension).ndkDirectory) + commandLine("./build-android.sh") +} +val buildSecp256k1AndroidX86_64 by creatingBuildSecp256k1Android("x86_64") +val buildSecp256k1AndroidX86 by creatingBuildSecp256k1Android("x86") +val buildSecp256k1AndroidArm64v8a by creatingBuildSecp256k1Android("arm64-v8a") +val buildSecp256k1AndroidArmeabiv7a by creatingBuildSecp256k1Android("armeabi-v7a") + +val clean by tasks.creating { + doLast { + delete(projectDir.resolve("build")) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index bd63c82..3caba25 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,12 +5,10 @@ pluginManagement { gradlePluginPortal() jcenter() } - - resolutionStrategy { - eachPlugin { - if (requested.id.id == "com.android.library") useModule("com.android.tools.build:gradle:${requested.version}") - } - } } rootProject.name = "secp256k1-kmp" +include( + ":native", + ":jni" +) \ No newline at end of file diff --git a/src/androidMain/CMakeLists.txt b/src/androidMain/CMakeLists.txt deleted file mode 100644 index a2063a8..0000000 --- a/src/androidMain/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -cmake_minimum_required(VERSION 3.10.0) - -add_library( secp256k1-jni SHARED - ${CMAKE_CURRENT_LIST_DIR}/../../native/jni/src/org_bitcoin_NativeSecp256k1.c - ${CMAKE_CURRENT_LIST_DIR}/../../native/jni/src/org_bitcoin_Secp256k1Context.c -) - -target_include_directories( secp256k1-jni - PUBLIC ${CMAKE_CURRENT_LIST_DIR}/../../native/secp256k1 -) - -target_link_libraries( secp256k1-jni - ${CMAKE_CURRENT_LIST_DIR}/../../native/build/android/${ANDROID_ABI}/libsecp256k1.a -) diff --git a/src/androidMain/kotlin/fr/acinq/secp256k1/Secp256k1Loader.kt b/src/androidMain/kotlin/fr/acinq/secp256k1/Secp256k1Loader.kt deleted file mode 100644 index 75df1eb..0000000 --- a/src/androidMain/kotlin/fr/acinq/secp256k1/Secp256k1Loader.kt +++ /dev/null @@ -1,15 +0,0 @@ -package fr.acinq.secp256k1 - -import java.io.* -import java.util.* - -internal actual object Secp256k1Loader { - - @JvmStatic - @Synchronized - @Throws(Exception::class) - actual fun initialize() { - System.loadLibrary("secp256k1-jni") - } - -} diff --git a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt index a1be21d..43bd632 100644 --- a/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt +++ b/src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt @@ -17,7 +17,6 @@ package fr.acinq.secp256k1 import kotlin.jvm.JvmStatic -import kotlin.jvm.Synchronized public enum class SigFormat(internal val size: Int) { COMPACT(64), DER(72) } @@ -25,37 +24,54 @@ public enum class PubKeyFormat(internal val size: Int) { COMPRESSED(33), UNCOMPR public expect object Secp256k1 { + @JvmStatic public fun verify(data: ByteArray, signature: ByteArray, pub: ByteArray): Boolean + @JvmStatic public fun sign(data: ByteArray, sec: ByteArray, format: SigFormat): ByteArray + @JvmStatic public fun signatureNormalize(sig: ByteArray, format: SigFormat): Pair + @JvmStatic public fun secKeyVerify(seckey: ByteArray): Boolean + @JvmStatic public fun computePubkey(seckey: ByteArray, format: PubKeyFormat): ByteArray + @JvmStatic public fun parsePubkey(pubkey: ByteArray, format: PubKeyFormat): ByteArray + @JvmStatic public fun cleanup() + @JvmStatic public fun privKeyNegate(privkey: ByteArray): ByteArray + @JvmStatic public fun privKeyTweakMul(privkey: ByteArray, tweak: ByteArray): ByteArray + @JvmStatic public fun privKeyTweakAdd(privkey: ByteArray, tweak: ByteArray): ByteArray + @JvmStatic public fun pubKeyNegate(pubkey: ByteArray): ByteArray + @JvmStatic public fun pubKeyTweakAdd(pubkey: ByteArray, tweak: ByteArray): ByteArray + @JvmStatic public fun pubKeyTweakMul(pubkey: ByteArray, tweak: ByteArray): ByteArray + @JvmStatic public fun pubKeyAdd(pubkey1: ByteArray, pubkey2: ByteArray): ByteArray + @JvmStatic public fun createECDHSecret(seckey: ByteArray, pubkey: ByteArray): ByteArray + @JvmStatic public fun ecdsaRecover(sig: ByteArray, message: ByteArray, recid: Int, format: PubKeyFormat): ByteArray + @JvmStatic public fun randomize(seed: ByteArray): Boolean } \ No newline at end of file diff --git a/src/jvmMain/kotlin/fr/acinq/secp256k1/OSInfo.kt b/src/jvmMain/kotlin/fr/acinq/secp256k1/OSInfo.kt deleted file mode 100644 index 6fd7368..0000000 --- a/src/jvmMain/kotlin/fr/acinq/secp256k1/OSInfo.kt +++ /dev/null @@ -1,228 +0,0 @@ -package fr.acinq.secp256k1 - -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.util.* - -/*-------------------------------------------------------------------------- - * Copyright 2008 Taro L. Saito - * - * 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. - *--------------------------------------------------------------------------*/ /** - * Provides OS name and architecture name. - * - * @author leo - */ -@Suppress("DuplicatedCode") -internal object OSInfo { - private val archMapping = HashMap() - private const val X86 = "x86" - private const val X86_64 = "x86_64" - private const val IA64_32 = "ia64_32" - private const val IA64 = "ia64" - private const val PPC = "ppc" - private const val PPC64 = "ppc64" - - @JvmStatic val nativeSuffix: String get() = "$os-$arch" - - @JvmStatic val os: String get() = translateOSName(System.getProperty("os.name")) - - @JvmStatic val hardwareName: String get() = - try { - val p = Runtime.getRuntime().exec("uname -m") - p.waitFor() - val input = p.inputStream - input.use { - val b = ByteArrayOutputStream() - val buf = ByteArray(32) - var readLen = it.read(buf, 0, buf.size) - while (readLen >= 0) { - b.write(buf, 0, readLen) - readLen = it.read(buf, 0, buf.size) - } - b.toString() - } - } catch (e: Throwable) { - System.err.println("Error while running uname -m: " + e.message) - "unknown" - } - - @JvmStatic - private fun resolveArmArchType(): String { - if (System.getProperty("os.name").contains("Linux")) { - val armType = hardwareName - // armType (uname -m) can be armv5t, armv5te, armv5tej, armv5tejl, armv6, armv7, armv7l, aarch64, i686// ignored: fall back to "arm" arch (soft-float ABI) - // ignored: fall back to "arm" arch (soft-float ABI) - // determine if first JVM found uses ARM hard-float ABI - when { - armType.startsWith("armv6") -> { - // Raspberry PI - return "armv6" - } - armType.startsWith("armv7") -> { - // Generic - return "armv7" - } - armType.startsWith("armv5") -> { - // Use armv5, soft-float ABI - return "arm" - } - armType == "aarch64" -> { - // Use arm64 - return "arm64" - } - - // Java 1.8 introduces a system property to determine armel or armhf - // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8005545 - - // For java7, we stil need to if run some shell commands to determine ABI of JVM - else -> { - val abi = System.getProperty("sun.arch.abi") - if (abi != null && abi.startsWith("gnueabihf")) { - return "armv7" - } - - // For java7, we stil need to if run some shell commands to determine ABI of JVM - val javaHome = System.getProperty("java.home") - try { - // determine if first JVM found uses ARM hard-float ABI - var exitCode = Runtime.getRuntime().exec("which readelf").waitFor() - if (exitCode == 0) { - val cmdarray = arrayOf( - "/bin/sh", "-c", "find '" + javaHome + - "' -name 'libjvm.so' | head -1 | xargs readelf -A | " + - "grep 'Tag_ABI_VFP_args: VFP registers'" - ) - exitCode = Runtime.getRuntime().exec(cmdarray).waitFor() - if (exitCode == 0) { - return "armv7" - } - } else { - System.err.println( - "WARNING! readelf not found. Cannot check if running on an armhf system, " + - "armel architecture will be presumed." - ) - } - } catch (e: IOException) { - // ignored: fall back to "arm" arch (soft-float ABI) - } catch (e: InterruptedException) { - // ignored: fall back to "arm" arch (soft-float ABI) - } - } - } - - // Java 1.8 introduces a system property to determine armel or armhf - // http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8005545 - val abi = System.getProperty("sun.arch.abi") - if (abi != null && abi.startsWith("gnueabihf")) { - return "armv7" - } - - // For java7, we stil need to if run some shell commands to determine ABI of JVM - val javaHome = System.getProperty("java.home") - try { - // determine if first JVM found uses ARM hard-float ABI - var exitCode = Runtime.getRuntime().exec("which readelf").waitFor() - if (exitCode == 0) { - val cmdarray = arrayOf( - "/bin/sh", "-c", "find '" + javaHome + - "' -name 'libjvm.so' | head -1 | xargs readelf -A | " + - "grep 'Tag_ABI_VFP_args: VFP registers'" - ) - exitCode = Runtime.getRuntime().exec(cmdarray).waitFor() - if (exitCode == 0) { - return "armv7" - } - } else { - System.err.println( - "WARNING! readelf not found. Cannot check if running on an armhf system, " + - "armel architecture will be presumed." - ) - } - } catch (e: IOException) { - // ignored: fall back to "arm" arch (soft-float ABI) - } catch (e: InterruptedException) { - // ignored: fall back to "arm" arch (soft-float ABI) - } - } - // Use armv5, soft-float ABI - return "arm" - } - - // For Android - @JvmStatic - val arch: String? - get() { - val systemOsArch = System.getProperty("os.arch") - val osArch = - if (systemOsArch.startsWith("arm")) { - resolveArmArchType() - } else { - val lc = systemOsArch.toLowerCase(Locale.US) - if (archMapping.containsKey(lc)) return archMapping[lc] - systemOsArch - } - return translateArchNameToFolderName(osArch) - } - - @JvmStatic - fun translateOSName(osName: String): String = - when { - osName.contains("Windows") -> "mingw" - osName.contains("Mac") || osName.contains("Darwin") -> "darwin" - osName.contains("Linux") -> "linux" - osName.contains("AIX") -> "aix" - else -> osName.replace("\\W".toRegex(), "") - } - - @JvmStatic - fun translateArchNameToFolderName(archName: String): String = archName.replace("\\W".toRegex(), "") - - init { - // x86 mappings - archMapping[X86] = X86 - archMapping["i386"] = X86 - archMapping["i486"] = X86 - archMapping["i586"] = X86 - archMapping["i686"] = X86 - archMapping["pentium"] = X86 - - // x86_64 mappings - archMapping[X86_64] = X86_64 - archMapping["amd64"] = X86_64 - archMapping["em64t"] = X86_64 - archMapping["universal"] = X86_64 // Needed for openjdk7 in Mac - - // Itenium 64-bit mappings - archMapping[IA64] = IA64 - archMapping["ia64w"] = IA64 - - // Itenium 32-bit mappings, usually an HP-UX construct - archMapping[IA64_32] = IA64_32 - archMapping["ia64n"] = IA64_32 - - // PowerPC mappings - archMapping[PPC] = PPC - archMapping["power"] = PPC - archMapping["powerpc"] = PPC - archMapping["power_pc"] = PPC - archMapping["power_rs"] = PPC - - // TODO: PowerPC 64bit mappings - archMapping[PPC64] = PPC64 - archMapping["power64"] = PPC64 - archMapping["powerpc64"] = PPC64 - archMapping["power_pc64"] = PPC64 - archMapping["power_rs64"] = PPC64 - } -} \ No newline at end of file diff --git a/src/jvmAndAndroidMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt b/src/jvmMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt similarity index 88% rename from src/jvmAndAndroidMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt rename to src/jvmMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt index aa19c98..6c9031d 100644 --- a/src/jvmAndAndroidMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt +++ b/src/jvmMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt @@ -17,15 +17,18 @@ package fr.acinq.secp256k1 import org.bitcoin.NativeSecp256k1 - -internal expect object Secp256k1Loader { - fun initialize() -} +import java.lang.IllegalStateException public actual object Secp256k1 { init { - Secp256k1Loader.initialize() + try { + val cls = Class.forName("fr.acinq.secp256k1.jni.NativeSecp256k1Loader") + val load = cls.getMethod("load") + load.invoke(null) + } catch (ex: ClassNotFoundException) { + throw IllegalStateException("Could not load native Secp256k1 JNI library. Have you added the JNI dependency?", ex) + } } public actual fun verify(data: ByteArray, signature: ByteArray, pub: ByteArray): Boolean = NativeSecp256k1.verify(data, signature, pub) diff --git a/src/jvmMain/kotlin/fr/acinq/secp256k1/Secp256k1Loader.kt b/src/jvmMain/kotlin/fr/acinq/secp256k1/Secp256k1Loader.kt deleted file mode 100644 index 954ca19..0000000 --- a/src/jvmMain/kotlin/fr/acinq/secp256k1/Secp256k1Loader.kt +++ /dev/null @@ -1,218 +0,0 @@ -package fr.acinq.secp256k1 - -import java.io.* -import java.util.* - -/*-------------------------------------------------------------------------- - * Copyright 2007 Taro L. Saito - * - * 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. - *--------------------------------------------------------------------------*/ /** - * Set the system properties, org.sqlite.lib.path, org.sqlite.lib.name, - * appropriately so that the SQLite JDBC driver can find *.dll, *.jnilib and - * *.so files, according to the current OS (win, linux, mac). - * The library files are automatically extracted from this project's package (JAR). - * usage: call [.initialize] before using SQLite JDBC driver. - * - * @author leo - */ -internal actual object Secp256k1Loader { - private var extracted = false - - /** - * Loads secp256k1 native library. - * - * @return True if secp256k1 native library is successfully loaded; false otherwise. - * @throws Exception if loading fails - */ - @JvmStatic - @Synchronized - @Throws(Exception::class) - actual fun initialize() { - // only cleanup before the first extract - if (!extracted) { - cleanup() - } - loadSecp256k1NativeLibrary() - } - - private val tempDir: File - get() = File(System.getProperty("fr.acinq.secp256k1.tmpdir", System.getProperty("java.io.tmpdir"))) - - /** - * Deleted old native libraries e.g. on Windows the DLL file is not removed - * on VM-Exit (bug #80) - */ - @JvmStatic - fun cleanup() { - val tempFolder = tempDir.absolutePath - val dir = File(tempFolder) - val nativeLibFiles = dir.listFiles(object : FilenameFilter { - private val searchPattern = "secp256k1-" - override fun accept(dir: File, name: String): Boolean { - return name.startsWith(searchPattern) && !name.endsWith(".lck") - } - }) - if (nativeLibFiles != null) { - for (nativeLibFile in nativeLibFiles) { - val lckFile = File(nativeLibFile.absolutePath + ".lck") - if (!lckFile.exists()) { - try { - nativeLibFile.delete() - } catch (e: SecurityException) { - System.err.println("Failed to delete old native lib" + e.message) - } - } - } - } - } - - @Throws(IOException::class) - private fun InputStream.contentsEquals(other: InputStream): Boolean { - val bufThis = this as? BufferedInputStream ?: BufferedInputStream(this) - val bufOther = other as? BufferedInputStream ?: BufferedInputStream(other) - var ch = bufThis.read() - while (ch != -1) { - val ch2 = bufOther.read() - if (ch != ch2) return false - ch = bufThis.read() - } - val ch2 = bufOther.read() - return ch2 == -1 - } - - /** - * Extracts and loads the specified library file to the target folder - * - * @param libDir Library path. - * @param libFileName Library name. - * @param targetDirectory Target folder. - * @return - */ - private fun extractAndLoadLibraryFile(libDir: String, libFileName: String, targetDirectory: String): Boolean { - val libPath = "$libDir/$libFileName" - // Include architecture name in temporary filename in order to avoid conflicts - // when multiple JVMs with different architectures running at the same time - val uuid = UUID.randomUUID().toString() - val extractedLibFileName = String.format("secp256k1-%s-%s", uuid, libFileName) - val extractedLckFileName = "$extractedLibFileName.lck" - val extractedLibFile = File(targetDirectory, extractedLibFileName) - val extractedLckFile = File(targetDirectory, extractedLckFileName) - return try { - // Extract a native library file into the target directory - val reader = Secp256k1Loader::class.java.getResourceAsStream(libPath) - if (!extractedLckFile.exists()) { - FileOutputStream(extractedLckFile).close() - } - val writer = FileOutputStream(extractedLibFile) - try { - val buffer = ByteArray(8192) - var bytesRead = reader.read(buffer) - while (bytesRead != -1) { - writer.write(buffer, 0, bytesRead) - bytesRead = reader.read(buffer) - } - } finally { - // Delete the extracted lib file on JVM exit. - extractedLibFile.deleteOnExit() - extractedLckFile.deleteOnExit() - writer.close() - reader.close() - } - - // Set executable (x) flag to enable Java to load the native library - extractedLibFile.setReadable(true) - extractedLibFile.setWritable(true, true) - extractedLibFile.setExecutable(true) - - // Check whether the contents are properly copied from the resource folder - Secp256k1Loader::class.java.getResourceAsStream(libPath).use { nativeIn -> - FileInputStream(extractedLibFile).use { extractedLibIn -> - if (!nativeIn.contentsEquals(extractedLibIn)) { - throw RuntimeException( - String.format( - "Failed to write a native library file at %s", - extractedLibFile - ) - ) - } - } - } - - loadNativeLibrary(targetDirectory, extractedLibFileName) - } catch (e: IOException) { - System.err.println(e.message) - false - } - } - - /** - * Loads native library using the given path and name of the library. - * - * @param path Path of the native library. - * @param name Name of the native library. - * @return True for successfully loading; false otherwise. - */ - private fun loadNativeLibrary(path: String, name: String): Boolean { - val libPath = File(path, name) - return if (libPath.exists()) { - try { - System.load(File(path, name).absolutePath) - true - } catch (e: UnsatisfiedLinkError) { - System.err.println("Failed to load native library:$name. osinfo: ${OSInfo.nativeSuffix}") - System.err.println(e) - false - } - } else { - false - } - } - - /** - * Loads secp256k1 native library using given path and name of the library. - * - * @throws - */ - private fun loadSecp256k1NativeLibrary() { - if (extracted) { - return - } - - // Try loading library from fr.acinq.secp256k1.lib.path library path */ - val libraryPath = System.getProperty("fr.acinq.secp256k1.lib.path") - val libraryName = System.getProperty("fr.acinq.secp256k1.lib.name") ?: System.mapLibraryName("secp256k1-jni-${OSInfo.nativeSuffix}") - if (libraryPath != null) { - if (loadNativeLibrary(libraryPath, libraryName)) { - extracted = true - return - } - } - - // Load the os-dependent library from the jar file - val packagePath = Secp256k1Loader::class.java.getPackage().name.replace("\\.".toRegex(), "/") - val embeddedLibraryPath = "/$packagePath/native" - val hasNativeLib = Secp256k1Loader::class.java.getResource("$embeddedLibraryPath/$libraryName") != null - if (!hasNativeLib) { - error("No native library found: at $embeddedLibraryPath/$libraryName") - } - - // Try extracting the library from jar - if (extractAndLoadLibraryFile(embeddedLibraryPath, libraryName, tempDir.absolutePath)) { - extracted = true - return - } - extracted = false - return - } -} diff --git a/src/jvmMain/kotlin/org/bitcoin/NativeSecp256k1.kt b/src/jvmMain/kotlin/org/bitcoin/NativeSecp256k1.kt new file mode 100644 index 0000000..9c39f10 --- /dev/null +++ b/src/jvmMain/kotlin/org/bitcoin/NativeSecp256k1.kt @@ -0,0 +1,565 @@ +/* + * Copyright 2013 Google Inc. + * Copyright 2014-2016 the libsecp256k1 contributors + * + * 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. + */ +package org.bitcoin + +import org.bitcoin.NativeSecp256k1Util.AssertFailException +import java.math.BigInteger +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantReadWriteLock + +/** + * + * This class holds native methods to handle ECDSA verification. + * + * + * You can find an example library that can be used for this at https://github.com/bitcoin/secp256k1 + * + * + * To build secp256k1 for use with bitcoinj, run + * `./configure --enable-jni --enable-experimental --enable-module-ecdh` + * and `make` then copy `.libs/libsecp256k1.so` to your system library path + * or point the JVM to the folder containing it with -Djava.library.path + * + */ +public object NativeSecp256k1 { + private val rwl = ReentrantReadWriteLock() + private val r: Lock = rwl.readLock() + private val w: Lock = rwl.writeLock() + private val nativeECDSABuffer = ThreadLocal() + + private fun pack(vararg buffers: ByteArray): ByteBuffer { + var size = 0 + for (i in buffers.indices) { + size += buffers[i].size + } + + val byteBuff = nativeECDSABuffer.get()?.takeIf { it.capacity() >= size } + ?: ByteBuffer.allocateDirect(size).also { + it.order(ByteOrder.nativeOrder()) + nativeECDSABuffer.set(it) + } + byteBuff.rewind() + for (i in buffers.indices) { + byteBuff.put(buffers[i]) + } + return byteBuff + } + + /** + * Verifies the given secp256k1 signature in native code. + * Calling when enabled == false is undefined (probably library not loaded) + * + * @param data The data which was signed, must be exactly 32 bytes + * @param signature The signature + * @param pub The public key which did the signing + * @return true if the signature is valid + * @throws AssertFailException in case of failure + */ + @JvmStatic + @Throws(AssertFailException::class) + public fun verify(data: ByteArray, signature: ByteArray, pub: ByteArray): Boolean { + require(data.size == 32 && signature.size <= 520 && pub.size <= 520) + val byteBuff = pack(data, signature, pub) + r.lock() + return try { + secp256k1_ecdsa_verify( + byteBuff, + Secp256k1Context.getContext(), + signature.size, + pub.size + ) == 1 + } finally { + r.unlock() + } + } + + /** + * libsecp256k1 Create an ECDSA signature. + * + * @param data Message hash, 32 bytes + * @param sec Secret key, 32 bytes + * @param compact True for compact signature, false for DER + * @return a signature, or an empty array is signing failed + * @throws AssertFailException in case of failure + */ + @JvmStatic + @Throws(AssertFailException::class) + public fun sign(data: ByteArray, sec: ByteArray, compact: Boolean): ByteArray { + require(data.size == 32 && sec.size <= 32) + val byteBuff = pack(data, sec) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_ecdsa_sign( + byteBuff, + compact, + Secp256k1Context.getContext() + ) + } finally { + r.unlock() + } + val sigArr = retByteArray[0] + val sigLen = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + NativeSecp256k1Util.assertEquals( + sigArr.size, + sigLen, + "Got bad signature length." + ) + return if (retVal == 0) ByteArray(0) else sigArr + } + + @JvmStatic + @Throws(AssertFailException::class) + public fun signatureNormalize(sig: ByteArray, compact: Boolean): Pair { + require(sig.size == 64 || sig.size in 70..73) + val byteBuff = pack(sig) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_ecdsa_normalize( + byteBuff, + sig.size, + compact, + Secp256k1Context.getContext() + ) + } finally { + r.unlock() + } + val sigArr = retByteArray[0] + val sigLen = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + val retBool = BigInteger(byteArrayOf(retByteArray[1][2])).toInt() + NativeSecp256k1Util.assertEquals( + sigArr.size, + sigLen, + "Got bad signature length." + ) + return (if (retVal == 0) ByteArray(0) else sigArr) to (retBool == 1) + } + + /** + * libsecp256k1 Seckey Verify - returns 1 if valid, 0 if invalid + * + * @param seckey ECDSA Secret key, 32 bytes + * @return true if seckey is valid + */ + @JvmStatic + public fun secKeyVerify(seckey: ByteArray): Boolean { + require(seckey.size == 32) + val byteBuff = pack(seckey) + r.lock() + return try { + secp256k1_ec_seckey_verify( + byteBuff, + Secp256k1Context.getContext() + ) == 1 + } finally { + r.unlock() + } + } + + /** + * libsecp256k1 Compute Pubkey - computes public key from secret key + * + * @param seckey ECDSA Secret key, 32 bytes + * @throws AssertFailException if parameters are not valid + * @return the corresponding public key (uncompressed) + */ + //TODO add a 'compressed' arg + @JvmStatic + @Throws(AssertFailException::class) + public fun computePubkey(seckey: ByteArray, compressed: Boolean): ByteArray { + require(seckey.size == 32) + val byteBuff = pack(seckey) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_ec_pubkey_create( + byteBuff, + compressed, + Secp256k1Context.getContext() + ) + } finally { + r.unlock() + } + val pubArr = retByteArray[0] + val pubLen = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + NativeSecp256k1Util.assertEquals(pubArr.size, pubLen, "Got bad pubkey length.") + return if (retVal == 0) ByteArray(0) else pubArr + } + + /** + * @param pubkey public key + * @return the input public key (uncompressed) if valid, or an empty array + * @throws AssertFailException in case of failure + */ + @JvmStatic + @Throws(AssertFailException::class) + public fun parsePubkey(pubkey: ByteArray, compressed: Boolean): ByteArray { + require(pubkey.size == 33 || pubkey.size == 65) + val byteBuff = pack(pubkey) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_ec_pubkey_parse( + byteBuff, + Secp256k1Context.getContext(), + pubkey.size, + compressed + ) + } finally { + r.unlock() + } + val pubArr = retByteArray[0] + BigInteger(byteArrayOf(retByteArray[1][0])).toInt() + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + NativeSecp256k1Util.assertEquals( + pubArr.size, + if (compressed) 33 else 65, + "Got bad pubkey length." + ) + return if (retVal == 0) ByteArray(0) else pubArr + } + + /** + * libsecp256k1 Cleanup - This destroys the secp256k1 context object + * This should be called at the end of the program for proper cleanup of the context. + */ + @JvmStatic + @Synchronized + public fun cleanup() { + w.lock() + try { + secp256k1_destroy_context(Secp256k1Context.getContext()) + } finally { + w.unlock() + } + } + + @JvmStatic + @Throws(AssertFailException::class) + public fun privKeyNegate(privkey: ByteArray): ByteArray { + require(privkey.size == 32) + val byteBuff = pack(privkey) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_privkey_negate( + byteBuff, + Secp256k1Context.getContext() + ) + } finally { + r.unlock() + } + val privArr = retByteArray[0] + val privLen: Int = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() and 0xFF + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + NativeSecp256k1Util.assertEquals( + privArr.size, + privLen, + "Got bad privkey length." + ) + NativeSecp256k1Util.assertEquals(retVal, 1, "Failed return value check.") + return privArr + } + + /** + * libsecp256k1 PrivKey Tweak-Mul - Tweak privkey by multiplying to it + * + * @param privkey 32-byte seckey + * @param tweak some bytes to tweak with + * @return privkey * tweak + * @throws AssertFailException in case of failure + */ + @JvmStatic + @Throws(AssertFailException::class) + public fun privKeyTweakMul(privkey: ByteArray, tweak: ByteArray): ByteArray { + require(privkey.size == 32) + val byteBuff = pack(privkey, tweak) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_privkey_tweak_mul( + byteBuff, + Secp256k1Context.getContext() + ) + } finally { + r.unlock() + } + val privArr = retByteArray[0] + val privLen: Int = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() and 0xFF + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + NativeSecp256k1Util.assertEquals( + privArr.size, + privLen, + "Got bad privkey length." + ) + NativeSecp256k1Util.assertEquals(retVal, 1, "Failed return value check.") + return privArr + } + + /** + * libsecp256k1 PrivKey Tweak-Add - Tweak privkey by adding to it + * + * @param privkey 32-byte seckey + * @param tweak some bytes to tweak with + * @return privkey + tweak + * @throws AssertFailException in case of failure + */ + @JvmStatic + @Throws(AssertFailException::class) + public fun privKeyTweakAdd(privkey: ByteArray, tweak: ByteArray): ByteArray { + require(privkey.size == 32) + val byteBuff = pack(privkey, tweak) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_privkey_tweak_add( + byteBuff, + Secp256k1Context.getContext() + ) + } finally { + r.unlock() + } + val privArr = retByteArray[0] + val privLen: Int = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() and 0xFF + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + NativeSecp256k1Util.assertEquals(privArr.size, privLen, "Got bad pubkey length.") + NativeSecp256k1Util.assertEquals(retVal, 1, "Failed return value check.") + return privArr + } + + @JvmStatic + @Throws(AssertFailException::class) + public fun pubKeyNegate(pubkey: ByteArray): ByteArray { + require(pubkey.size == 33 || pubkey.size == 65) + val byteBuff = pack(pubkey) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_pubkey_negate( + byteBuff, + Secp256k1Context.getContext(), + pubkey.size + ) + } finally { + r.unlock() + } + val pubArr = retByteArray[0] + val pubLen: Int = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() and 0xFF + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + NativeSecp256k1Util.assertEquals(pubArr.size, pubLen, "Got bad pubkey length.") + NativeSecp256k1Util.assertEquals(retVal, 1, "Failed return value check.") + return pubArr + } + + /** + * libsecp256k1 PubKey Tweak-Add - Tweak pubkey by adding to it + * + * @param tweak some bytes to tweak with + * @param pubkey 32-byte seckey + * @return pubkey + tweak + * @throws AssertFailException in case of failure + */ + @JvmStatic + @Throws(AssertFailException::class) + public fun pubKeyTweakAdd(pubkey: ByteArray, tweak: ByteArray): ByteArray { + require(pubkey.size == 33 || pubkey.size == 65) + val byteBuff = pack(pubkey, tweak) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_pubkey_tweak_add( + byteBuff, + Secp256k1Context.getContext(), + pubkey.size + ) + } finally { + r.unlock() + } + val pubArr = retByteArray[0] + val pubLen: Int = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() and 0xFF + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + NativeSecp256k1Util.assertEquals(pubArr.size, pubLen, "Got bad pubkey length.") + NativeSecp256k1Util.assertEquals(retVal, 1, "Failed return value check.") + return pubArr + } + + /** + * libsecp256k1 PubKey Tweak-Mul - Tweak pubkey by multiplying to it + * + * @param tweak some bytes to tweak with + * @param pubkey 32-byte seckey + * @return pubkey * tweak + * @throws AssertFailException in case of failure + */ + @JvmStatic + @Throws(AssertFailException::class) + public fun pubKeyTweakMul(pubkey: ByteArray, tweak: ByteArray): ByteArray { + require(pubkey.size == 33 || pubkey.size == 65) + val byteBuff = pack(pubkey, tweak) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_pubkey_tweak_mul( + byteBuff, + Secp256k1Context.getContext(), + pubkey.size + ) + } finally { + r.unlock() + } + val pubArr = retByteArray[0] + val pubLen: Int = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() and 0xFF + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + NativeSecp256k1Util.assertEquals(pubArr.size, pubLen, "Got bad pubkey length.") + NativeSecp256k1Util.assertEquals(retVal, 1, "Failed return value check.") + return pubArr + } + + @JvmStatic + @Throws(AssertFailException::class) + public fun pubKeyAdd(pubkey1: ByteArray, pubkey2: ByteArray): ByteArray { + require(pubkey1.size == 33 || pubkey1.size == 65) + require(pubkey2.size == 33 || pubkey2.size == 65) + val byteBuff = pack(pubkey1, pubkey2) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_ec_pubkey_add( + byteBuff, + Secp256k1Context.getContext(), + pubkey1.size, + pubkey2.size + ) + } finally { + r.unlock() + } + val pubArr = retByteArray[0] + val pubLen: Int = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() and 0xFF + val retVal = BigInteger(byteArrayOf(retByteArray[1][1])).toInt() + NativeSecp256k1Util.assertEquals(pubkey1.size, pubLen, "Got bad pubkey length.") + NativeSecp256k1Util.assertEquals(retVal, 1, "Failed return value check.") + return pubArr + } + + /** + * libsecp256k1 create ECDH secret - constant time ECDH calculation + * + * @param seckey byte array of secret key used in exponentiaion + * @param pubkey byte array of public key used in exponentiaion + * @return ecdh(sedckey, pubkey) + * @throws AssertFailException in case of failure + */ + @JvmStatic + @Throws(AssertFailException::class) + public fun createECDHSecret(seckey: ByteArray, pubkey: ByteArray): ByteArray { + require(seckey.size <= 32 && pubkey.size <= 65) + val byteBuff = pack(seckey, pubkey) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_ecdh( + byteBuff, + Secp256k1Context.getContext(), + pubkey.size + ) + } finally { + r.unlock() + } + val resArr = retByteArray[0] + val retVal = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() + NativeSecp256k1Util.assertEquals(resArr.size, 32, "Got bad result length.") + NativeSecp256k1Util.assertEquals(retVal, 1, "Failed return value check.") + return resArr + } + + @JvmStatic + @Throws(AssertFailException::class) + public fun ecdsaRecover(sig: ByteArray, message: ByteArray, recid: Int, compressed: Boolean): ByteArray { + require(sig.size == 64) + require(message.size == 32) + val byteBuff = pack(sig, message) + val retByteArray: Array + r.lock() + retByteArray = try { + secp256k1_ecdsa_recover( + byteBuff, + Secp256k1Context.getContext(), + recid, + compressed + ) + } finally { + r.unlock() + } + val resArr = retByteArray[0] + val retVal = BigInteger(byteArrayOf(retByteArray[1][0])).toInt() + NativeSecp256k1Util.assertEquals( + resArr.size, + if (compressed) 33 else 65, + "Got bad result length." + ) + NativeSecp256k1Util.assertEquals(retVal, 1, "Failed return value check.") + return resArr + } + + /** + * libsecp256k1 randomize - updates the context randomization + * + * @param seed 32-byte random seed + * @return true if successful + * @throws AssertFailException in case of failure + */ + @JvmStatic + @Synchronized + @Throws(AssertFailException::class) + public fun randomize(seed: ByteArray): Boolean { + require(seed.size == 32) + val byteBuff = pack(seed) + w.lock() + return try { + secp256k1_context_randomize( + byteBuff, + Secp256k1Context.getContext() + ) == 1 + } finally { + w.unlock() + } + } + + @JvmStatic private external fun secp256k1_context_randomize(byteBuff: ByteBuffer, context: Long): Int + @JvmStatic private external fun secp256k1_privkey_negate(byteBuff: ByteBuffer, context: Long): Array + @JvmStatic private external fun secp256k1_privkey_tweak_add(byteBuff: ByteBuffer, context: Long): Array + @JvmStatic private external fun secp256k1_privkey_tweak_mul(byteBuff: ByteBuffer, context: Long): Array + @JvmStatic private external fun secp256k1_pubkey_negate(byteBuff: ByteBuffer, context: Long, pubLen: Int): Array + @JvmStatic private external fun secp256k1_pubkey_tweak_add(byteBuff: ByteBuffer, context: Long, pubLen: Int): Array + @JvmStatic private external fun secp256k1_pubkey_tweak_mul(byteBuff: ByteBuffer, context: Long, pubLen: Int): Array + @JvmStatic private external fun secp256k1_destroy_context(context: Long) + @JvmStatic private external fun secp256k1_ecdsa_verify(byteBuff: ByteBuffer, context: Long, sigLen: Int, pubLen: Int): Int + @JvmStatic private external fun secp256k1_ecdsa_sign(byteBuff: ByteBuffer, compact: Boolean, context: Long): Array + @JvmStatic private external fun secp256k1_ecdsa_normalize(byteBuff: ByteBuffer, sigLen: Int, compact: Boolean, context: Long): Array + @JvmStatic private external fun secp256k1_ec_seckey_verify(byteBuff: ByteBuffer, context: Long): Int + @JvmStatic private external fun secp256k1_ec_pubkey_create(byteBuff: ByteBuffer, compressed: Boolean, context: Long): Array + @JvmStatic private external fun secp256k1_ec_pubkey_parse(byteBuff: ByteBuffer, context: Long, inputLen: Int, compressed: Boolean): Array + @JvmStatic private external fun secp256k1_ec_pubkey_add(byteBuff: ByteBuffer, context: Long, lent1: Int, len2: Int): Array + @JvmStatic private external fun secp256k1_ecdh(byteBuff: ByteBuffer, context: Long, inputLen: Int): Array + @JvmStatic private external fun secp256k1_ecdsa_recover(byteBuff: ByteBuffer, context: Long, recid: Int, compressed: Boolean): Array +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/org/bitcoin/NativeSecp256k1Util.kt b/src/jvmMain/kotlin/org/bitcoin/NativeSecp256k1Util.kt new file mode 100644 index 0000000..40c689e --- /dev/null +++ b/src/jvmMain/kotlin/org/bitcoin/NativeSecp256k1Util.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2014-2016 the libsecp256k1 contributors + * + * 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. + */ +package org.bitcoin + +import kotlin.jvm.Throws +import java.lang.Exception + +internal object NativeSecp256k1Util { + @Throws(AssertFailException::class) + fun assertEquals(val1: Int, val2: Int, message: String) { + if (val1 != val2) throw AssertFailException("FAIL: $message") + } + + class AssertFailException(message: String?) : Exception(message) +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/org/bitcoin/Secp256k1Context.kt b/src/jvmMain/kotlin/org/bitcoin/Secp256k1Context.kt new file mode 100644 index 0000000..13deb5b --- /dev/null +++ b/src/jvmMain/kotlin/org/bitcoin/Secp256k1Context.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2014-2016 the libsecp256k1 contributors + * + * 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. + */ +package org.bitcoin + +/** + * This class holds the context reference used in native methods + * to handle ECDSA operations. + */ +public object Secp256k1Context { + @JvmStatic + public val isEnabled: Boolean //true if the library is loaded + private val context: Long //ref to pointer to context obj + + @JvmStatic + public fun getContext(): Long { + return if (!isEnabled) -1 else context //sanity check + } + + @JvmStatic private external fun secp256k1_init_context(): Long + + init { //static initializer + isEnabled = true + context = + secp256k1_init_context() + } +}