diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 8b26ff656..496170a69 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -63,6 +63,17 @@ jobs: with: xcode-version: '15.0.1' + - name: "Install rust toolchain (Linux)" + if: matrix.os-type == 'linux' + run: sudo apt install rustc build-essential -y + + - name: "Install rust toolchain (Macos)" + if: matrix.os-type == 'macos' + run: brew install rustup + + - name: "Install wasm-pack" + run: cargo install wasm-pack + - name: "Test Kotlin code is properly formatted" run: ./gradlew ktlintCheck diff --git a/.github/workflows/release-documentation.yml b/.github/workflows/release-documentation.yml index 18eeb4c3f..4787c2567 100644 --- a/.github/workflows/release-documentation.yml +++ b/.github/workflows/release-documentation.yml @@ -56,6 +56,17 @@ jobs: run: | brew install autoconf automake libtool + - name: "Install rust toolchain (Linux)" + if: matrix.os-type == 'linux' + run: sudo apt install rustc build-essential -y + + - name: "Install rust toolchain (Macos)" + if: matrix.os-type == 'macos' + run: brew install rustup + + - name: "Install wasm-pack" + run: cargo install wasm-pack + - name: "Dokka Documentation Generation" run: | ./gradlew dokkaHtml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33b086d28..4b0538bf8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,17 @@ jobs: git_user_signingkey: true git_commit_gpgsign: true + - name: "Install rust toolchain (Linux)" + if: matrix.os-type == 'linux' + run: sudo apt install rustc build-essential -y + + - name: "Install rust toolchain (Macos)" + if: matrix.os-type == 'macos' + run: brew install rustup + + - name: "Install wasm-pack" + run: cargo install wasm-pack + - name: "Release" env: GIT_AUTHOR_EMAIL: ${{ steps.import_gpg.outputs.email }} diff --git a/.gitmodules b/.gitmodules index db8b83818..477ea4dc0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,8 @@ path = secp256k1-kmp/native/secp256k1 url = https://github.com/bitcoin-core/secp256k1 branch = master + +[submodule "rust-ed25519-bip32"] + path = rust-ed25519-bip32 + url = https://github.com/input-output-hk/rust-ed25519-bip32.git + branch = master diff --git a/apollo/build.gradle.kts b/apollo/build.gradle.kts index 31a836505..e59d4e496 100644 --- a/apollo/build.gradle.kts +++ b/apollo/build.gradle.kts @@ -1,11 +1,13 @@ import dev.petuska.npm.publish.extension.domain.NpmAccess import org.gradle.internal.os.OperatingSystem import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput.Target import org.jetbrains.kotlin.gradle.tasks.CInteropProcess import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jlleitschuh.gradle.ktlint.tasks.KtLintCheckTask import java.net.URL import java.time.Year @@ -13,6 +15,14 @@ val currentModuleName: String = "Apollo" val os: OperatingSystem = OperatingSystem.current() val secp256k1Dir = rootDir.resolve("secp256k1-kmp") +val taskGroup = "build ed25519-bip32" +val ed25519bip32Dir = rootDir.resolve("rust-ed25519-bip32") +val generatedDir = project.layout.buildDirectory.asFile.get().resolve("generated") +val generatedResourcesDir = project.layout.buildDirectory.asFile.get().resolve("generatedResources") +val ed25519bip32BinariesDir = ed25519bip32Dir.resolve("wrapper").resolve("target") +val ANDROID_SDK = System.getenv("ANDROID_HOME") +val NDK = System.getenv("ANDROID_NDK_HOME") + plugins { kotlin("multiplatform") id("io.github.luca992.multiplatform-swiftpackage") version "2.2.2" @@ -70,6 +80,126 @@ fun KotlinNativeTarget.secp256k1CInterop(target: String) { } } +/** + * Generates the cinterop configuration for the ed25519Bip32 library based on the target platform. + * + * @param target The target platform for which the cinterop configuration is generated. + * Supported values are "macosX64", "macosArm64", "iosArm64", "iosX64", and "iosSimulatorArm64". + * + * @throws GradleException if an unsupported target platform is specified. + */ +fun KotlinNativeTarget.ed25519Bip32CInterop(target: String) { + compilations.getByName("main") { + cinterops { + val ed25519_bip32_wrapper by creating { + val crate = this.name + packageName("$crate.cinterop") + header( + project.layout.buildDirectory.asFile.get() + .resolve("generated") + .resolve("nativeInterop") + .resolve("cinterop") + .resolve("headers") + .resolve(crate) + .resolve("$crate.h") + ) + tasks.named(interopProcessingTaskName) { + dependsOn(":apollo:buildEd25519Bip32") + } + when (target) { + "macosX64" -> { + extraOpts( + "-libraryPath", + rootDir + .resolve("rust-ed25519-bip32") + .resolve("wrapper") + .resolve("target") + .resolve("x86_64-apple-darwin") + .resolve("release") + .absolutePath + ) + } + + "macosArm64" -> { + extraOpts( + "-libraryPath", + rootDir + .resolve("rust-ed25519-bip32") + .resolve("wrapper") + .resolve("target") + .resolve("aarch64-apple-darwin") + .resolve("release") + .absolutePath + ) + } + + "iosArm64" -> { + extraOpts( + "-libraryPath", + rootDir + .resolve("rust-ed25519-bip32") + .resolve("wrapper") + .resolve("target") + .resolve("aarch64-apple-ios") + .resolve("release") + .absolutePath + ) + } + + "iosX64" -> { + extraOpts( + "-libraryPath", + rootDir + .resolve("rust-ed25519-bip32") + .resolve("wrapper") + .resolve("target") + .resolve("x86_64-apple-ios") + .resolve("release") + .absolutePath + ) + } + + "iosSimulatorArm64" -> { + extraOpts( + "-libraryPath", + rootDir + .resolve("rust-ed25519-bip32") + .resolve("wrapper") + .resolve("target") + .resolve("aarch64-apple-ios-sim") + .resolve("release") + .absolutePath + ) + } + + else -> { + throw GradleException("Unsupported linking for $target") + } + } + } + } + } +} + +/** + * Creates a copy task with the specified parameters. + * + * @param name The name of the copy task. + * @param fromDir The source directory from which files will be copied. + * @param intoDir The destination directory where files will be copied into. + */ +fun createCopyTask( + name: String, + fromDir: File, + intoDir: File +) = tasks.register(name) { + group = taskGroup + duplicatesStrategy = DuplicatesStrategy.INCLUDE + include("*.so", "*.a", "*.d", "*.dylib", "**/*.kt", "*.js", "**/*.h") + from(fromDir) + into(intoDir) +} + /** * The `javadocJar` variable is used to register a `Jar` task to generate a Javadoc JAR file. * The Javadoc JAR file is created with the classifier "javadoc" and it includes the HTML documentation generated @@ -80,6 +210,156 @@ val javadocJar by tasks.registering(Jar::class) { from(tasks.dokkaHtml) } +/** + * Copy Generated Kotlin Code + */ +val copyEd25519Bip32GeneratedTask = createCopyTask( + "copyEd25519Bip32Generated", + ed25519bip32Dir.resolve("wrapper").resolve("build").resolve("generated"), + generatedDir +) + +/** + * Copy JVM Code to Android folder + */ +val copyToAndroidSrc by tasks.register("copyToAndroidSrc") { + group = taskGroup + duplicatesStrategy = DuplicatesStrategy.INCLUDE + include("*.so", "*.a", "*.d", "*.dylib", "**/*.kt", "*.js", "**/*.h") + from(project.layout.buildDirectory.asFile.get().resolve("generated").resolve("jvmMain")) + into(project.layout.buildDirectory.asFile.get().resolve("generated").resolve("androidMain")) + dependsOn(copyEd25519Bip32GeneratedTask) + mustRunAfter(copyEd25519Bip32GeneratedTask) +} + +val copyEd25519Bip32ForMacOSX8664Task = createCopyTask( + "copyEd25519Bip32ForMacOSX86_64", + ed25519bip32BinariesDir.resolve("x86_64-apple-darwin").resolve("release"), + generatedResourcesDir.resolve("jvm").resolve("main").resolve("darwin-x86-64") +) + +val copyEd25519Bip32ForMacOSArch64Task = createCopyTask( + "copyEd25519Bip32ForMacOSArch64", + ed25519bip32BinariesDir.resolve("aarch64-apple-darwin").resolve("release"), + generatedResourcesDir.resolve("jvm").resolve("main").resolve("darwin-aarch64") +) + +val copyEd25519Bip32ForLinuxX8664Task = createCopyTask( + "copyEd25519Bip32ForLinuxX86_64", + ed25519bip32BinariesDir.resolve("x86_64-unknown-linux-gnu").resolve("release"), + generatedResourcesDir.resolve("jvm").resolve("main").resolve("linux-x86-64") +) + +val copyEd25519Bip32ForLinuxArch64Task = createCopyTask( + "copyEd25519Bip32ForLinuxArch64", + ed25519bip32BinariesDir.resolve("aarch64-unknown-linux-gnu").resolve("release"), + generatedResourcesDir.resolve("jvm").resolve("main").resolve("linux-aarch64") +) + +/** + * A task group that responsible for moving generated JVM binaries to correct folder. + */ +val copyEd25519Bip32ForJVMTargetTask by tasks.register("copyEd25519Bip32ForJVMTarget") { + group = taskGroup + dependsOn(copyEd25519Bip32ForMacOSX8664Task, copyEd25519Bip32ForMacOSArch64Task, copyEd25519Bip32ForLinuxX8664Task, copyEd25519Bip32ForLinuxArch64Task) +} + +val copyEd25519Bip32ForAndroidX8664Task = createCopyTask( + "copyEd25519Bip32ForAndroidX86_64", + ed25519bip32BinariesDir.resolve("x86_64-linux-android").resolve("release"), + generatedResourcesDir.resolve("android").resolve("main").resolve("jniLibs").resolve("x86_64") +) + +val copyEd25519Bip32ForAndroidArch64Task = createCopyTask( + "copyEd25519Bip32ForAndroidArch64", + ed25519bip32BinariesDir.resolve("aarch64-linux-android").resolve("release"), + generatedResourcesDir.resolve("android").resolve("main").resolve("jniLibs").resolve("arm64-v8a") +) + +val copyEd25519Bip32ForAndroidI686Task = createCopyTask( + "copyEd25519Bip32ForAndroidI686", + ed25519bip32BinariesDir.resolve("i686-linux-android").resolve("release"), + generatedResourcesDir.resolve("android").resolve("main").resolve("jniLibs").resolve("x86") +) + +val copyEd25519Bip32ForAndroidArmv7aTask = createCopyTask( + "copyEd25519Bip32ForAndroidArmv7a", + ed25519bip32BinariesDir.resolve("armv7-linux-androideabi").resolve("release"), + generatedResourcesDir.resolve("android").resolve("main").resolve("jniLibs").resolve("armeabi-v7a") +) + +/** + * A task group that responsible for moving generated Android binaries to correct folder. + */ +val copyEd25519Bip32ForAndroidTargetTask by tasks.register("copyEd25519Bip32ForAndroidTarget") { + group = taskGroup + dependsOn(copyEd25519Bip32ForAndroidX8664Task, copyEd25519Bip32ForAndroidArch64Task, copyEd25519Bip32ForAndroidI686Task, copyEd25519Bip32ForAndroidArmv7aTask) +} + +val copyEd25519Bip32Wrapper by tasks.register("copyEd25519Bip32") { + dependsOn( + copyEd25519Bip32GeneratedTask, + copyEd25519Bip32ForJVMTargetTask, + copyEd25519Bip32ForAndroidTargetTask, + copyToAndroidSrc + ) + mustRunAfter(buildEd25519Bip32Wrapper) +} + +val buildEd25519Bip32Wrapper by tasks.register("buildEd25519Bip32Wrapper") { + group = taskGroup + workingDir = ed25519bip32Dir.resolve("wrapper") + val localEnv = this.environment + localEnv += mapOf( + "ANDROID_HOME" to ANDROID_SDK, + "ANDROID_NDK_HOME" to NDK + ) + this.environment = localEnv + commandLine("./build-kotlin-library.sh") +} + +val copyEd25519Bip32Wasm = createCopyTask( + "copyEd25519Bip32GeneratedWasm", + ed25519bip32Dir.resolve("wasm").resolve("build"), + rootDir.resolve("build").resolve("js").resolve("packages").resolve("Apollo").resolve("kotlin") +) +copyEd25519Bip32Wasm.configure { + mustRunAfter(buildEd25519Bip32Wasm) +} + +val copyEd25519Bip32WasmTest = createCopyTask( + "copyEd25519Bip32GeneratedWasmTest", + ed25519bip32Dir.resolve("wasm").resolve("build"), + rootDir.resolve("build").resolve("js").resolve("packages").resolve("Apollo-test").resolve("kotlin") +) +copyEd25519Bip32WasmTest.configure { + mustRunAfter(buildEd25519Bip32Wasm) +} + +val buildEd25519Bip32Wasm by tasks.register("buildEd25519Bip32Wasm") { + group = taskGroup + workingDir = ed25519bip32Dir.resolve("wasm") + commandLine("./build_kotlin_library.sh") +} + +val buildEd25519Bip32Task by tasks.register("buildEd25519Bip32") { + group = taskGroup + dependsOn(buildEd25519Bip32Wasm, copyEd25519Bip32Wasm, copyEd25519Bip32WasmTest, buildEd25519Bip32Wrapper, copyEd25519Bip32Wrapper) +} + +val cleanEd25519Bip32 by tasks.register("cleanEd25519Bip32") { + group = taskGroup + val wasmDir = ed25519bip32Dir.resolve("wasm") + delete.add(wasmDir.resolve("build")) + delete.add(wasmDir.resolve("pkg")) + delete.add(wasmDir.resolve("node_modules")) + delete.add(wasmDir.resolve("target")) + + val wrapperDir = ed25519bip32Dir.resolve("wrapper") + delete.add(wrapperDir.resolve("build")) + delete.add(wrapperDir.resolve("target")) +} + kotlin { androidTarget { publishAllLibraryVariants() @@ -107,6 +387,8 @@ kotlin { swiftCinterop("IOHKCryptoKit", name) secp256k1CInterop("ios") + ed25519Bip32CInterop(name) + // https://youtrack.jetbrains.com/issue/KT-39396 compilations["main"].kotlinOptions.freeCompilerArgs += listOf( "-include-binary", @@ -122,7 +404,6 @@ kotlin { binaries.framework { baseName = "ApolloLibrary" embedBitcode(BitcodeEmbeddingMode.DISABLE) - freeCompilerArgs += listOf("-Xoverride-konan-properties=minVersion.ios=13.0;minVersionSinceXcode15.ios=13.0") } } iosX64 { @@ -130,6 +411,8 @@ kotlin { swiftCinterop("IOHKCryptoKit", name) secp256k1CInterop("ios") + ed25519Bip32CInterop(name) + // https://youtrack.jetbrains.com/issue/KT-39396 compilations["main"].kotlinOptions.freeCompilerArgs += listOf( "-include-binary", @@ -145,7 +428,6 @@ kotlin { binaries.framework { baseName = "ApolloLibrary" embedBitcode(BitcodeEmbeddingMode.DISABLE) - freeCompilerArgs += listOf("-Xoverride-konan-properties=minVersion.ios=13.0;minVersionSinceXcode15.ios=13.0") if (os.isMacOsX) { if (System.getenv().containsKey("XCODE_VERSION_MAJOR") && System.getenv("XCODE_VERSION_MAJOR") == "1500") { linkerOpts += "-ld64" @@ -158,6 +440,8 @@ kotlin { swiftCinterop("IOHKCryptoKit", name) secp256k1CInterop("ios") + ed25519Bip32CInterop(name) + // https://youtrack.jetbrains.com/issue/KT-39396 compilations["main"].kotlinOptions.freeCompilerArgs += listOf( "-include-binary", @@ -173,7 +457,6 @@ kotlin { binaries.framework { baseName = "ApolloLibrary" embedBitcode(BitcodeEmbeddingMode.DISABLE) - freeCompilerArgs += listOf("-Xoverride-konan-properties=minVersion.ios_simulator_arm64=13.0;minVersionSinceXcode15.ios=13.0") } } macosArm64 { @@ -181,6 +464,8 @@ kotlin { swiftCinterop("IOHKCryptoKit", name) secp256k1CInterop("macosArm64") + ed25519Bip32CInterop(name) + // https://youtrack.jetbrains.com/issue/KT-39396 compilations["main"].kotlinOptions.freeCompilerArgs += listOf( "-include-binary", @@ -196,7 +481,6 @@ kotlin { binaries.framework { baseName = "ApolloLibrary" embedBitcode(BitcodeEmbeddingMode.DISABLE) - freeCompilerArgs += listOf("-Xoverride-konan-properties=minVersion.macos=11.0;minVersionSinceXcode15.macos=11.0") } } js(IR) { @@ -248,6 +532,8 @@ kotlin { implementation("com.ionspin.kotlin:bignum:0.3.9") implementation("org.kotlincrypto.macs:hmac-sha2:0.3.0") implementation("org.kotlincrypto.hash:sha2:0.4.0") + implementation("com.squareup.okio:okio:3.7.0") + implementation("org.jetbrains.kotlinx:atomicfu:0.23.1") } } val commonTest by getting { @@ -255,7 +541,30 @@ kotlin { implementation(kotlin("test")) } } + val allButJSMain by creating { + dependsOn(commonMain) + kotlin.srcDir( + generatedDir + .resolve("commonMain") + .resolve("kotlin") + ) + } + val allButJSTest by creating { + dependsOn(commonTest) + } val androidMain by getting { + dependsOn(allButJSMain) + kotlin.srcDir( + generatedDir + .resolve("androidMain") + .resolve("kotlin") + ) + val generatedResources = project.layout.buildDirectory.asFile.get() + .resolve("generatedResources") + .resolve("android") + .resolve("main") + .resolve("jniLibs") + resources.srcDir(generatedResources) dependencies { api("fr.acinq.secp256k1:secp256k1-kmp:0.14.0") implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:0.11.0") @@ -263,23 +572,38 @@ kotlin { implementation("com.google.guava:guava:30.1-jre") implementation("org.bouncycastle:bcprov-jdk15on:1.68") implementation("org.bitcoinj:bitcoinj-core:0.16.2") + implementation("net.java.dev.jna:jna:5.13.0@aar") } } val androidUnitTest by getting { + dependsOn(allButJSTest) dependencies { implementation("junit:junit:4.13.2") } } val jvmMain by getting { + dependsOn(allButJSMain) + kotlin.srcDir( + generatedDir + .resolve("jvmMain") + .resolve("kotlin") + ) + val generatedResources = project.layout.buildDirectory.asFile.get() + .resolve("generatedResources") + .resolve("jvm") + .resolve("main") + resources.srcDir(generatedResources) dependencies { api("fr.acinq.secp256k1:secp256k1-kmp:0.14.0") implementation("fr.acinq.secp256k1:secp256k1-kmp-jni-jvm:0.11.0") implementation("com.google.guava:guava:30.1-jre") implementation("org.bouncycastle:bcprov-jdk15on:1.68") implementation("org.bitcoinj:bitcoinj-core:0.16.2") + implementation("net.java.dev.jna:jna:5.13.0") } } val jvmTest by getting { + dependsOn(allButJSTest) dependencies { implementation("junit:junit:4.13.2") } @@ -302,7 +626,14 @@ kotlin { } } val jsTest by getting - + val nativeMain by getting { + dependsOn(allButJSMain) + kotlin.srcDir( + generatedDir + .resolve("nativeMain") + .resolve("kotlin") + ) + } val appleMain by getting { kotlin.srcDirs( secp256k1Dir @@ -315,10 +646,22 @@ kotlin { .resolve("kotlin") ) } + + all { + languageSettings { + optIn("kotlin.RequiresOptIn") + optIn("kotlinx.cinterop.ExperimentalForeignApi") + } + } + } + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") } // Enable the export of KDoc (Experimental feature) to Generated Native targets (Apple, Linux, etc.) - targets.withType { + targets.withType { compilations.getByName("main") { compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc") } @@ -338,6 +681,18 @@ android { namespace = "io.iohk.atala.prism.apollo" compileSdk = 34 sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + sourceSets["main"].jniLibs { + setSrcDirs( + listOf( + project.layout.buildDirectory.asFile.get() + .resolve("generatedResources") + .resolve("android") + .resolve("main") + .resolve("jniLibs") + ) + ) + } + defaultConfig { minSdk = 21 } @@ -360,41 +715,11 @@ android { } } -afterEvaluate { - tasks.withType { - dependsOn( - ":iOSLibs:buildIOHKCryptoKitIphoneos", - ":iOSLibs:buildIOHKCryptoKitIphonesimulator", - ":iOSLibs:buildIOHKCryptoKitMacosx", - ":iOSLibs:buildIOHKSecureRandomGenerationIphoneos", - ":iOSLibs:buildIOHKSecureRandomGenerationIphonesimulator", - ":iOSLibs:buildIOHKSecureRandomGenerationMacosx" - ) - } - tasks.withType { - dependsOn( - ":iOSLibs:buildIOHKCryptoKitIphoneos", - ":iOSLibs:buildIOHKCryptoKitIphonesimulator", - ":iOSLibs:buildIOHKCryptoKitMacosx", - ":iOSLibs:buildIOHKSecureRandomGenerationIphoneos", - ":iOSLibs:buildIOHKSecureRandomGenerationIphonesimulator", - ":iOSLibs:buildIOHKSecureRandomGenerationMacosx" - ) - } - tasks.withType { - dependsOn( - ":iOSLibs:buildIOHKCryptoKitIphoneos", - ":iOSLibs:buildIOHKCryptoKitIphonesimulator", - ":iOSLibs:buildIOHKCryptoKitMacosx", - ":iOSLibs:buildIOHKSecureRandomGenerationIphoneos", - ":iOSLibs:buildIOHKSecureRandomGenerationIphonesimulator", - ":iOSLibs:buildIOHKSecureRandomGenerationMacosx" - ) - } -} - ktlint { filter { + exclude { + it.file.toString().contains("generated") + } exclude("**/external/*", "./src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/external/*") exclude { it.file.toString().contains("external") @@ -479,6 +804,54 @@ npmPublish { // Workaround for a bug in Gradle afterEvaluate { + tasks.withType { + dependsOn( + ":iOSLibs:buildIOHKCryptoKitIphoneos", + ":iOSLibs:buildIOHKCryptoKitIphonesimulator", + ":iOSLibs:buildIOHKCryptoKitMacosx", + ":iOSLibs:buildIOHKSecureRandomGenerationIphoneos", + ":iOSLibs:buildIOHKSecureRandomGenerationIphonesimulator", + ":iOSLibs:buildIOHKSecureRandomGenerationMacosx", + buildEd25519Bip32Task + ) + } + tasks.withType { + dependsOn( + ":iOSLibs:buildIOHKCryptoKitIphoneos", + ":iOSLibs:buildIOHKCryptoKitIphonesimulator", + ":iOSLibs:buildIOHKCryptoKitMacosx", + ":iOSLibs:buildIOHKSecureRandomGenerationIphoneos", + ":iOSLibs:buildIOHKSecureRandomGenerationIphonesimulator", + ":iOSLibs:buildIOHKSecureRandomGenerationMacosx", + buildEd25519Bip32Task + ) + } + tasks.withType { + dependsOn( + ":iOSLibs:buildIOHKCryptoKitIphoneos", + ":iOSLibs:buildIOHKCryptoKitIphonesimulator", + ":iOSLibs:buildIOHKCryptoKitMacosx", + ":iOSLibs:buildIOHKSecureRandomGenerationIphoneos", + ":iOSLibs:buildIOHKSecureRandomGenerationIphonesimulator", + ":iOSLibs:buildIOHKSecureRandomGenerationMacosx", + copyEd25519Bip32GeneratedTask + ) + } + tasks.getByName("clean") { + // dependsOn(cleanEd25519Bip32) + } + tasks.withType { + // dependsOn(buildEd25519Bip32Task) + } + tasks.getByName("mergeDebugJniLibFolders").dependsOn(buildEd25519Bip32Task) + tasks.getByName("mergeReleaseJniLibFolders").dependsOn(buildEd25519Bip32Task) + tasks.getByName("packageDebugResources").dependsOn(buildEd25519Bip32Task) + tasks.getByName("packageReleaseResources").dependsOn(buildEd25519Bip32Task) + tasks.getByName("extractDeepLinksForAarDebug").dependsOn(buildEd25519Bip32Task) + tasks.getByName("extractDeepLinksForAarRelease").dependsOn(buildEd25519Bip32Task) + tasks.getByName("mergeDebugResources").dependsOn(buildEd25519Bip32Task) + tasks.getByName("mergeReleaseResources").dependsOn(buildEd25519Bip32Task) + if (tasks.findByName("iosX64Test") != null) { tasks.named("iosX64Test") { this.enabled = false diff --git a/apollo/src/androidInstrumentedTest/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKeyTest.kt b/apollo/src/androidInstrumentedTest/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKeyTest.kt new file mode 100644 index 000000000..8c52d31f7 --- /dev/null +++ b/apollo/src/androidInstrumentedTest/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKeyTest.kt @@ -0,0 +1,21 @@ +package io.iohk.atala.prism.apollo.derivation + +import io.iohk.atala.prism.apollo.utils.decodeHex +import io.iohk.atala.prism.apollo.utils.toHexString +import kotlin.test.Test +import kotlin.test.assertEquals + +@RunWith(AndroidJUnit4::class) +class EdHDKeyTest { + @Test + fun test_derive_m1() { + val privateKey = "f8a29231ee38d6c5bf715d5bac21c750577aa3798b22d79d65bf97d6fadea15adcd1ee1abdf78bd4be64731a12deb94d3671784112eb6f364b871851fd1c9a24".decodeHex() + val chainCode = "7384db9ad6003bbd08b3b1ddc0d07a597293ff85e961bf252b331262eddfad0d".decodeHex() + + val key = EdHDKey(privateKey, chainCode) + val derived = key.derive("m/1'") + + assertEquals(derived.privateKey.toHexString(), "4057eb6cab9000e3b6fe7e556341da1ca2f5dde0b689a7b58cb93f1902dfa15a5a10732ff348051c6e0865c62931d4a73fa8050b8ff543b43fc0000a7e2c5700") + assertEquals(derived.chainCode.toHexString(), "9a170f689c8b9b3502ee846f457ab3dd1b017cfb2cd68865c7f24dbabcbc2256") + } +} diff --git a/apollo/src/androidMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt b/apollo/src/androidMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt new file mode 100644 index 000000000..68d68624d --- /dev/null +++ b/apollo/src/androidMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt @@ -0,0 +1,100 @@ +package io.iohk.atala.prism.apollo.derivation + +import com.ionspin.kotlin.bignum.integer.toBigInteger +import ed25519_bip32_wrapper.deriveBytes +import ed25519_bip32_wrapper.fromNonextendedNoforce +import io.iohk.atala.prism.apollo.utils.ECConfig + +/** + * Represents and HDKey with its derive methods + */ +actual class EdHDKey actual constructor( + actual val privateKey: ByteArray, + actual val chainCode: ByteArray, + actual val depth: Int, + actual val index: BigIntegerWrapper +) { + /** + * Method to derive an HDKey by a path + * + * @param path value used to derive a key + */ + actual fun derive(path: String): EdHDKey { + if (!path.matches(Regex("^[mM].*"))) { + throw Error("Path must start with \"m\" or \"M\"") + } + if (Regex("^[mM]'?$").matches(path)) { + return this + } + val parts = path.replace(Regex("^[mM]'?/"), "").split("/") + var child = this + + for (c in parts) { + val m = Regex("^(\\d+)('?)$").find(c)?.groupValues + if (m == null || m.size != 3) { + throw Error("Invalid child index: $c") + } + val idx = m[1].toBigInteger() + if (idx >= HDKey.HARDENED_OFFSET) { + throw Error("Invalid index") + } + val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx + + child = child.deriveChild(BigIntegerWrapper(finalIdx)) + } + + return child + } + + /** + * Method to derive an HDKey child by index + * + * @param wrappedIndex value used to derive a key + */ + actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDKey { + val index = wrappedIndex.value.uintValue() + val derived = deriveBytes(privateKey, chainCode, index) + val secretKey = derived["secret_key"] + val chainCode = derived["chain_code"] + + if (secretKey == null || chainCode == null) { + throw Error("Unable to derive key") + } + + return EdHDKey( + privateKey = secretKey, + chainCode = chainCode, + depth = depth + 1, + index = wrappedIndex + ) + } + + actual companion object { + /** + * Constructs a new EdHDKey object from a seed + * + * @param seed The seed used to derive the private key and chain code. + * @throws IllegalArgumentException if the seed length is not equal to 64. + */ + actual fun initFromSeed(seed: ByteArray): EdHDKey { + require(seed.size == 64) { + "Seed expected byte length to be ${ECConfig.PRIVATE_KEY_BYTE_SIZE}" + } + + val keySlice = seed.sliceArray(0 until 32) + val chainCodeSlice = seed.sliceArray(32 until seed.size) + val result = fromNonextendedNoforce(keySlice, chainCodeSlice) + val secretKey = result["secret_key"] + val chainCode = result["chain_code"] + + if (secretKey == null || chainCode == null) { + throw Error("Unable to derive key") + } + + return EdHDKey( + privateKey = secretKey, + chainCode = chainCode + ) + } + } +} diff --git a/apollo/src/androidMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt b/apollo/src/androidMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt index 8a5729ce8..60473db6e 100644 --- a/apollo/src/androidMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt +++ b/apollo/src/androidMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt @@ -33,4 +33,14 @@ actual class KMMEdPrivateKey(val raw: ByteArray) { signer.update(message, 0, message.size) return signer.generateSignature() } + + /** + * Method convert an ed25519 private key to a x25519 private key + * + * @return KMMX25519PrivateKey private key + */ + actual fun x25519PrivateKey(): KMMX25519PrivateKey { + val rawX25519Prv = convertSecretKeyToX25519(this.raw) + return KMMX25519PrivateKey(rawX25519Prv) + } } diff --git a/apollo/src/appleMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt b/apollo/src/appleMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt new file mode 100644 index 000000000..68d68624d --- /dev/null +++ b/apollo/src/appleMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt @@ -0,0 +1,100 @@ +package io.iohk.atala.prism.apollo.derivation + +import com.ionspin.kotlin.bignum.integer.toBigInteger +import ed25519_bip32_wrapper.deriveBytes +import ed25519_bip32_wrapper.fromNonextendedNoforce +import io.iohk.atala.prism.apollo.utils.ECConfig + +/** + * Represents and HDKey with its derive methods + */ +actual class EdHDKey actual constructor( + actual val privateKey: ByteArray, + actual val chainCode: ByteArray, + actual val depth: Int, + actual val index: BigIntegerWrapper +) { + /** + * Method to derive an HDKey by a path + * + * @param path value used to derive a key + */ + actual fun derive(path: String): EdHDKey { + if (!path.matches(Regex("^[mM].*"))) { + throw Error("Path must start with \"m\" or \"M\"") + } + if (Regex("^[mM]'?$").matches(path)) { + return this + } + val parts = path.replace(Regex("^[mM]'?/"), "").split("/") + var child = this + + for (c in parts) { + val m = Regex("^(\\d+)('?)$").find(c)?.groupValues + if (m == null || m.size != 3) { + throw Error("Invalid child index: $c") + } + val idx = m[1].toBigInteger() + if (idx >= HDKey.HARDENED_OFFSET) { + throw Error("Invalid index") + } + val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx + + child = child.deriveChild(BigIntegerWrapper(finalIdx)) + } + + return child + } + + /** + * Method to derive an HDKey child by index + * + * @param wrappedIndex value used to derive a key + */ + actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDKey { + val index = wrappedIndex.value.uintValue() + val derived = deriveBytes(privateKey, chainCode, index) + val secretKey = derived["secret_key"] + val chainCode = derived["chain_code"] + + if (secretKey == null || chainCode == null) { + throw Error("Unable to derive key") + } + + return EdHDKey( + privateKey = secretKey, + chainCode = chainCode, + depth = depth + 1, + index = wrappedIndex + ) + } + + actual companion object { + /** + * Constructs a new EdHDKey object from a seed + * + * @param seed The seed used to derive the private key and chain code. + * @throws IllegalArgumentException if the seed length is not equal to 64. + */ + actual fun initFromSeed(seed: ByteArray): EdHDKey { + require(seed.size == 64) { + "Seed expected byte length to be ${ECConfig.PRIVATE_KEY_BYTE_SIZE}" + } + + val keySlice = seed.sliceArray(0 until 32) + val chainCodeSlice = seed.sliceArray(32 until seed.size) + val result = fromNonextendedNoforce(keySlice, chainCodeSlice) + val secretKey = result["secret_key"] + val chainCode = result["chain_code"] + + if (secretKey == null || chainCode == null) { + throw Error("Unable to derive key") + } + + return EdHDKey( + privateKey = secretKey, + chainCode = chainCode + ) + } + } +} diff --git a/apollo/src/appleMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt b/apollo/src/appleMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt index b6361ee6a..6ffb1031b 100644 --- a/apollo/src/appleMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt +++ b/apollo/src/appleMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt @@ -44,4 +44,14 @@ public actual class KMMEdPrivateKey(val raw: ByteArray) { val publicRaw = result.success()?.toByteArray() ?: throw RuntimeException("Null result") return KMMEdPublicKey(publicRaw) } + + /** + * Method convert an ed25519 private key to a x25519 private key + * + * @return KMMX25519PrivateKey private key + */ + actual fun x25519PrivateKey(): KMMX25519PrivateKey { + val rawX25519Prv = convertSecretKeyToX25519(this.raw) + return KMMX25519PrivateKey(rawX25519Prv) + } } diff --git a/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt b/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt new file mode 100644 index 000000000..dbbc87344 --- /dev/null +++ b/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt @@ -0,0 +1,40 @@ +package io.iohk.atala.prism.apollo.derivation + +/** + * Represents and HDKey with its derive methods + */ +expect class EdHDKey constructor( + privateKey: ByteArray, + chainCode: ByteArray, + depth: Int = 0, + index: BigIntegerWrapper = BigIntegerWrapper(0) +) { + val privateKey: ByteArray + val chainCode: ByteArray + val depth: Int + val index: BigIntegerWrapper + + /** + * Method to derive an HDKey by a path + * + * @param path value used to derive a key + */ + fun derive(path: String): EdHDKey + + /** + * Method to derive an HDKey child by index + * + * @param wrappedIndex value used to derive a key + */ + fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDKey + + companion object { + /** + * Constructs a new EdHDKey object from a seed + * + * @param seed The seed used to derive the private key and chain code. + * @throws IllegalArgumentException if the seed length is not equal to 64. + */ + fun initFromSeed(seed: ByteArray): EdHDKey + } +} diff --git a/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/ConvertEd25519.kt b/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/ConvertEd25519.kt new file mode 100644 index 000000000..f6e4b4846 --- /dev/null +++ b/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/ConvertEd25519.kt @@ -0,0 +1,23 @@ +package io.iohk.atala.prism.apollo.utils + +import org.kotlincrypto.hash.sha2.SHA512 +import kotlin.experimental.and +import kotlin.experimental.or + +/** + * This function converts an Ed25519 secret key into a Curve25519 secret key. + * Curve25519 keys are used in the key exchange process to encrypt communications over networks. + * + * @param secretKey The Ed25519 secret key to be converted into a Curve25519 secret key. + * @return The Curve25519 key, which is a 32-byte hash derived from the original Ed25519 key. + */ +fun convertSecretKeyToX25519(secretKey: ByteArray): ByteArray { + // Hash the first 32 bytes of the Ed25519 secret key + val hashed = SHA512().digest(secretKey.sliceArray(0 until 32)) + // Clamping the hashed value to conform with X25519 format + hashed[0] = hashed[0] and 248.toByte() + hashed[31] = hashed[31] and 127.toByte() + hashed[31] = hashed[31] or 64.toByte() + // Return the first 32 bytes of the hash as the X25519 secret key + return hashed.sliceArray(0 until 32) +} diff --git a/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdKeyPair.kt b/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdKeyPair.kt index d534ad53d..cb8f649fb 100644 --- a/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdKeyPair.kt +++ b/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdKeyPair.kt @@ -1,5 +1,7 @@ package io.iohk.atala.prism.apollo.utils +import kotlin.js.ExperimentalJsExport + /** * Interface defining the functionality for generating KMMEd key pairs. */ @@ -7,6 +9,7 @@ expect class KMMEdKeyPair(privateKey: KMMEdPrivateKey, publicKey: KMMEdPublicKey val privateKey: KMMEdPrivateKey val publicKey: KMMEdPublicKey + @OptIn(ExperimentalJsExport::class) companion object : Ed25519KeyPairGeneration /** diff --git a/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt b/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt index f4c61fc87..b063b4b78 100644 --- a/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt +++ b/apollo/src/commonMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt @@ -12,4 +12,11 @@ public expect class KMMEdPrivateKey { * @return ByteArray representing the signed message */ fun sign(message: ByteArray): ByteArray + + /** + * Method convert an ed25519 private key to a x25519 private key + * + * @return KMMX25519PrivateKey private key + */ + fun x25519PrivateKey(): KMMX25519PrivateKey } diff --git a/apollo/src/commonTest/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKeyTest.kt b/apollo/src/commonTest/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKeyTest.kt new file mode 100644 index 000000000..503875e63 --- /dev/null +++ b/apollo/src/commonTest/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKeyTest.kt @@ -0,0 +1,23 @@ +package io.iohk.atala.prism.apollo.derivation + +import io.iohk.atala.prism.apollo.Platform +import io.iohk.atala.prism.apollo.utils.decodeHex +import io.iohk.atala.prism.apollo.utils.toHexString +import kotlin.test.Test +import kotlin.test.assertEquals + +class EdHDKeyTest { + @Test + fun test_derive_m1() { + if (!Platform.OS.contains("Android")) { + val privateKey = "f8a29231ee38d6c5bf715d5bac21c750577aa3798b22d79d65bf97d6fadea15adcd1ee1abdf78bd4be64731a12deb94d3671784112eb6f364b871851fd1c9a24".decodeHex() + val chainCode = "7384db9ad6003bbd08b3b1ddc0d07a597293ff85e961bf252b331262eddfad0d".decodeHex() + + val key = EdHDKey(privateKey, chainCode) + val derived = key.derive("m/1'") + + assertEquals(derived.privateKey.toHexString(), "4057eb6cab9000e3b6fe7e556341da1ca2f5dde0b689a7b58cb93f1902dfa15a5a10732ff348051c6e0865c62931d4a73fa8050b8ff543b43fc0000a7e2c5700") + assertEquals(derived.chainCode.toHexString(), "9a170f689c8b9b3502ee846f457ab3dd1b017cfb2cd68865c7f24dbabcbc2256") + } + } +} diff --git a/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt b/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt new file mode 100644 index 000000000..022463536 --- /dev/null +++ b/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt @@ -0,0 +1,88 @@ +package io.iohk.atala.prism.apollo.derivation + +import com.ionspin.kotlin.bignum.integer.toBigInteger +import io.iohk.atala.prism.apollo.utils.ECConfig +import io.iohk.atala.prism.apollo.utils.external.ed25519_bip32 + +/** + * Represents and HDKey with its derive methods + */ +@OptIn(ExperimentalJsExport::class) +@JsExport +actual class EdHDKey actual constructor( + actual val privateKey: ByteArray, + actual val chainCode: ByteArray, + actual val depth: Int, + actual val index: BigIntegerWrapper +) { + /** + * Method to derive an HDKey by a path + * + * @param path value used to derive a key + */ + actual fun derive(path: String): EdHDKey { + if (!path.matches(Regex("^[mM].*"))) { + throw Error("Path must start with \"m\" or \"M\"") + } + if (Regex("^[mM]'?$").matches(path)) { + return this + } + val parts = path.replace(Regex("^[mM]'?/"), "").split("/") + var child = this + + for (c in parts) { + val m = Regex("^(\\d+)('?)$").find(c)?.groupValues + if (m == null || m.size != 3) { + throw Error("Invalid child index: $c") + } + val idx = m[1].toBigInteger() + if (idx >= HDKey.HARDENED_OFFSET) { + throw Error("Invalid index") + } + val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx + + child = child.deriveChild(BigIntegerWrapper(finalIdx)) + } + + return child + } + + /** + * Method to derive an HDKey child by index + * + * @param wrappedIndex value used to derive a key + */ + actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDKey { + val derived = ed25519_bip32.derive_bytes(privateKey, chainCode, wrappedIndex.value.uintValue()) + + return EdHDKey( + privateKey = derived[0], + chainCode = derived[1], + depth = depth + 1, + index = wrappedIndex + ) + } + + actual companion object { + /** + * Constructs a new EdHDKey object from a seed + * + * @param seed The seed used to derive the private key and chain code. + * @throws IllegalArgumentException if the seed length is not equal to 64. + */ + actual fun initFromSeed(seed: ByteArray): EdHDKey { + require(seed.size == 64) { + "Seed expected byte length to be ${ECConfig.PRIVATE_KEY_BYTE_SIZE}" + } + + val key = seed.sliceArray(0 until 32) + val chainCode = seed.sliceArray(32 until seed.size) + val result = ed25519_bip32.from_nonextended_noforce(key, chainCode) + + return EdHDKey( + privateKey = result[0], + chainCode = result[1] + ) + } + } +} diff --git a/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/Curve25519Parser.kt b/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/Curve25519Parser.kt index adb746576..b7ca8405f 100644 --- a/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/Curve25519Parser.kt +++ b/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/Curve25519Parser.kt @@ -1,6 +1,8 @@ package io.iohk.atala.prism.apollo.utils import io.iohk.atala.prism.apollo.base64.base64UrlDecodedBytes +import io.iohk.atala.prism.apollo.utils.Curve25519Parser.encodedLength +import io.iohk.atala.prism.apollo.utils.Curve25519Parser.rawLength import node.buffer.Buffer /** @@ -13,6 +15,7 @@ import node.buffer.Buffer @OptIn(ExperimentalJsExport::class) @JsExport object Curve25519Parser { + val extendedLength = 64 val encodedLength = 43 val rawLength = 32 @@ -29,6 +32,10 @@ object Curve25519Parser { return Buffer.from(buffer.toByteArray().decodeToString().base64UrlDecodedBytes) } + if (buffer.length == extendedLength) { + return buffer + } + if (buffer.length == rawLength) { return buffer } diff --git a/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt b/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt index 35f4b3584..90afe0546 100644 --- a/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt +++ b/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt @@ -51,4 +51,14 @@ actual class KMMEdPrivateKey(bytes: ByteArray) { return sig.toHex().encodeToByteArray() } + + /** + * Method convert an ed25519 private key to a x25519 private key + * + * @return KMMX25519PrivateKey private key + */ + actual fun x25519PrivateKey(): KMMX25519PrivateKey { + val rawX25519Prv = convertSecretKeyToX25519(this.raw.toByteArray()) + return KMMX25519PrivateKey(rawX25519Prv) + } } diff --git a/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/external/Ed25519_Bip32.kt b/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/external/Ed25519_Bip32.kt new file mode 100644 index 000000000..bda38b718 --- /dev/null +++ b/apollo/src/jsMain/kotlin/io/iohk/atala/prism/apollo/utils/external/Ed25519_Bip32.kt @@ -0,0 +1,11 @@ +package io.iohk.atala.prism.apollo.utils.external + +external interface ed25519_bip32_export { + fun from_nonextended_noforce(key: ByteArray, chain_code: ByteArray): Array + + fun derive_bytes(key: ByteArray, chain_code: ByteArray, index: Any): Array +} + +@JsModule("./ed25519_bip32_wasm.js") +@JsNonModule +external val ed25519_bip32: ed25519_bip32_export diff --git a/apollo/src/jvmMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt b/apollo/src/jvmMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt new file mode 100644 index 000000000..68d68624d --- /dev/null +++ b/apollo/src/jvmMain/kotlin/io/iohk/atala/prism/apollo/derivation/EdHDKey.kt @@ -0,0 +1,100 @@ +package io.iohk.atala.prism.apollo.derivation + +import com.ionspin.kotlin.bignum.integer.toBigInteger +import ed25519_bip32_wrapper.deriveBytes +import ed25519_bip32_wrapper.fromNonextendedNoforce +import io.iohk.atala.prism.apollo.utils.ECConfig + +/** + * Represents and HDKey with its derive methods + */ +actual class EdHDKey actual constructor( + actual val privateKey: ByteArray, + actual val chainCode: ByteArray, + actual val depth: Int, + actual val index: BigIntegerWrapper +) { + /** + * Method to derive an HDKey by a path + * + * @param path value used to derive a key + */ + actual fun derive(path: String): EdHDKey { + if (!path.matches(Regex("^[mM].*"))) { + throw Error("Path must start with \"m\" or \"M\"") + } + if (Regex("^[mM]'?$").matches(path)) { + return this + } + val parts = path.replace(Regex("^[mM]'?/"), "").split("/") + var child = this + + for (c in parts) { + val m = Regex("^(\\d+)('?)$").find(c)?.groupValues + if (m == null || m.size != 3) { + throw Error("Invalid child index: $c") + } + val idx = m[1].toBigInteger() + if (idx >= HDKey.HARDENED_OFFSET) { + throw Error("Invalid index") + } + val finalIdx = if (m[2] == "'") idx + HDKey.HARDENED_OFFSET else idx + + child = child.deriveChild(BigIntegerWrapper(finalIdx)) + } + + return child + } + + /** + * Method to derive an HDKey child by index + * + * @param wrappedIndex value used to derive a key + */ + actual fun deriveChild(wrappedIndex: BigIntegerWrapper): EdHDKey { + val index = wrappedIndex.value.uintValue() + val derived = deriveBytes(privateKey, chainCode, index) + val secretKey = derived["secret_key"] + val chainCode = derived["chain_code"] + + if (secretKey == null || chainCode == null) { + throw Error("Unable to derive key") + } + + return EdHDKey( + privateKey = secretKey, + chainCode = chainCode, + depth = depth + 1, + index = wrappedIndex + ) + } + + actual companion object { + /** + * Constructs a new EdHDKey object from a seed + * + * @param seed The seed used to derive the private key and chain code. + * @throws IllegalArgumentException if the seed length is not equal to 64. + */ + actual fun initFromSeed(seed: ByteArray): EdHDKey { + require(seed.size == 64) { + "Seed expected byte length to be ${ECConfig.PRIVATE_KEY_BYTE_SIZE}" + } + + val keySlice = seed.sliceArray(0 until 32) + val chainCodeSlice = seed.sliceArray(32 until seed.size) + val result = fromNonextendedNoforce(keySlice, chainCodeSlice) + val secretKey = result["secret_key"] + val chainCode = result["chain_code"] + + if (secretKey == null || chainCode == null) { + throw Error("Unable to derive key") + } + + return EdHDKey( + privateKey = secretKey, + chainCode = chainCode + ) + } + } +} diff --git a/apollo/src/jvmMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt b/apollo/src/jvmMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt index 6c2d48b26..6cfd35125 100644 --- a/apollo/src/jvmMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt +++ b/apollo/src/jvmMain/kotlin/io/iohk/atala/prism/apollo/utils/KMMEdPrivateKey.kt @@ -34,4 +34,14 @@ actual class KMMEdPrivateKey(val raw: ByteArray) { signer.update(message, 0, message.size) return signer.generateSignature() } + + /** + * Method convert an ed25519 private key to a x25519 private key + * + * @return KMMX25519PrivateKey private key + */ + actual fun x25519PrivateKey(): KMMX25519PrivateKey { + val rawX25519Prv = convertSecretKeyToX25519(this.raw) + return KMMX25519PrivateKey(rawX25519Prv) + } } diff --git a/apollo/src/nativeInterop/cinterop/ed25519_bip32_wrapper.def b/apollo/src/nativeInterop/cinterop/ed25519_bip32_wrapper.def new file mode 100644 index 000000000..36aeb1c0d --- /dev/null +++ b/apollo/src/nativeInterop/cinterop/ed25519_bip32_wrapper.def @@ -0,0 +1 @@ +staticLibraries = libed25519_bip32_wrapper.a \ No newline at end of file diff --git a/apollo/webpack.config.d/polyfill.js b/apollo/webpack.config.d/polyfill.js index f5b3bf1c8..7ee4c26bc 100644 --- a/apollo/webpack.config.d/polyfill.js +++ b/apollo/webpack.config.d/polyfill.js @@ -1,6 +1,7 @@ config.resolve = { fallback: { - stream: require.resolve("stream-browserify") + stream: require.resolve("stream-browserify"), + url: require.resolve("url") } }; var webpack = require('webpack'); diff --git a/build.gradle.kts b/build.gradle.kts index 2880cd19c..cc769f2c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ buildscript { mavenCentral() } dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") classpath("org.jetbrains.dokka:dokka-base:1.9.20") } } diff --git a/rust-ed25519-bip32 b/rust-ed25519-bip32 new file mode 160000 index 000000000..c7217751c --- /dev/null +++ b/rust-ed25519-bip32 @@ -0,0 +1 @@ +Subproject commit c7217751c278c4ed0e6c26e3ef9d6d14195582b9