From 5af44072bc4bf87074d7a6e1fe3d765f8e2f993c Mon Sep 17 00:00:00 2001 From: Michael T Chuang Date: Thu, 19 Dec 2024 13:49:24 -0800 Subject: [PATCH] PERA-1263 :: Add Bip39 libraries and create/recover Algo25 using AlgoSDK in common module (#59) --- app/build.gradle | 9 +++ .../src/androidMain/AndroidManifest.xml | 2 + .../res/xml/data_extraction_rules.xml | 28 +++++++ gradle/libs.versions.toml | 28 +++++-- wallet-sdk/build.gradle.kts | 15 ++-- .../common/algosdk/AlgoAccountSdk.android.kt | 79 +++++++++++-------- .../algorand/common/algosdk/AlgoAccountSdk.kt | 4 +- .../common/algosdk/AlgoAccountSdk.ios.kt | 4 +- 8 files changed, 115 insertions(+), 54 deletions(-) create mode 100644 composeTestApp/src/androidMain/res/xml/data_extraction_rules.xml diff --git a/app/build.gradle b/app/build.gradle index eadfc22c..478eb5a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -210,6 +210,15 @@ android { exclude 'lib/libnarcissus-win-64.dll' } + configurations.configureEach { + // exclude duplicate bouncycastle till walletconnect is no longer used, use + // common wallet-sdk module versions instead + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15to18' + exclude group: 'org.bouncycastle', module: 'bcutil-jdk18on' + exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' + exclude group: 'org.bouncycastle', module: 'bcutil-jdk15on' + } + namespace libs.versions.android.namespace.get().toString() } diff --git a/composeTestApp/src/androidMain/AndroidManifest.xml b/composeTestApp/src/androidMain/AndroidManifest.xml index 238de48a..d6605817 100644 --- a/composeTestApp/src/androidMain/AndroidManifest.xml +++ b/composeTestApp/src/androidMain/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + + + + + + + + + + + + + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0a1a3511..8ef460ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ androidx-compose-ui = "1.7.5" androidx-uiTest = "1.7.5" appCompat = "1.7.0" biometric = "1.1.0" +bip39 = "1.0.7" ble = "2.8.0" browser = "1.8.0" buildConfig = "4.1.1" @@ -75,12 +76,12 @@ sqlite = "2.5.0-alpha11" runner = "1.6.2" safeargs = "2.8.4" tink = "1.14.0" +xhdwalletapi = "1.1.0" walletConnectBom = "1.34.1" zxing = "4.3.0" [libraries] agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } -algosdk = { module = "com.algorand:algosdk", version.ref = "algosdk" } androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-compose-activity" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "composeFoundation" } androidx-compose-fragment = { module = "androidx.fragment:fragment-compose", version.ref = "androidx-compose-fragment" } @@ -116,8 +117,6 @@ firebase-messaging = { module = "com.google.firebase:firebase-messaging" } firebase-perf = { module = "com.google.firebase:firebase-perf" } flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" } fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" } -glide-image = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } -glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } google-services = { module = "com.google.gms:google-services", version.ref = "googleServicesVersion" } java-websocket = { module = "org.java-websocket:Java-WebSocket", version.ref = "javaWebSocket" } junit = { module = "junit:junit", version.ref = "junit" } @@ -163,14 +162,12 @@ moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpAndroidChart" } multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } -napier = { module = "io.github.aakira:napier", version.ref = "napier" } navigation-compose = { group = "org.jetbrains.androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" } navigation-fragment-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" } navigation-safe-args-gradle-plugin = { module = "androidx.navigation:navigation-safe-args-gradle-plugin", version.ref = "safeargs" } navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" } paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "paging" } perf-plugin = { module = "com.google.firebase:perf-plugin", version.ref = "firebasePerformance" } -retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } review-ktx = { module = "com.google.android.play:review-ktx", version.ref = "review" } sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" } room-gradle-plugin = { module = "androidx.room:room-gradle-plugin", version.ref = "room" } @@ -179,13 +176,28 @@ room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } room-testing = { module = "androidx.room:room-testing", version.ref = "room" } runner = { module = "androidx.test:runner", version.ref = "runner" } -sign = { module = "com.walletconnect:sign" } tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tink" } +zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing" } + +# Wallet SDK - Logging +napier = { module = "io.github.aakira:napier", version.ref = "napier" } + +# Wallet SDK - Security / Blockchain / Crypto +algosdk = { module = "com.algorand:algosdk", version.ref = "algosdk" } +# jna = "net.java.dev.jna:jna:5.14.0@aar" +xhdwalletapi = { module = "foundation.algorand.xhdwalletapi:xhdwalletapi-android", version.ref = "xhdwalletapi" } +kotlin-bip39 = { module = "cash.z.ecc.android:kotlin-bip39", version.ref = "bip39" } + +# App Module Only +glide-image = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } + +# Wallet Connect +sign = { module = "com.walletconnect:sign" } walletconnect = { module = "com.walletconnect:android-bom", version.ref = "walletConnectBom" } walletconnect-android-core = { module = "com.walletconnect:android-core" } web3wallet = { module = "com.walletconnect:web3wallet" } -zxing-android-embedded = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing" } - [bundles] firebase = ["firebase-analytics", diff --git a/wallet-sdk/build.gradle.kts b/wallet-sdk/build.gradle.kts index 0659cbb4..b576e970 100644 --- a/wallet-sdk/build.gradle.kts +++ b/wallet-sdk/build.gradle.kts @@ -40,6 +40,7 @@ kotlin { @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { languageVersion.set(KotlinVersion.KOTLIN_2_0) + freeCompilerArgs.add("-Xexpect-actual-classes") } listOf( @@ -55,8 +56,14 @@ kotlin { sourceSets { androidMain.dependencies { + // will turn this to implementation when app module references are moved to common api(libs.algosdk) + // toml files don't support aar files yet + implementation("net.java.dev.jna:jna:5.14.0@aar") + implementation(libs.xhdwalletapi) + implementation(libs.kotlin.bip39) + implementation(compose.uiTooling) implementation(libs.androidx.activityCompose) implementation(libs.androidx.compose.foundation) @@ -127,14 +134,6 @@ android { sourceSets["main"].res.srcDirs("src/commonMain/composeResources", "src/androidMain/res") sourceSets["main"].resources.srcDirs("src/commonMain/composeResources") - - configurations.all { - // exclude duplicate bouncycastle till walletconnect is no longer used - exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18") - exclude(group = "org.bouncycastle", module = "bcutil-jdk18on") - exclude(group = "org.bouncycastle", module = "bcprov-jdk15on") - exclude(group = "org.bouncycastle", module = "bcutil-jdk15on") - } } room { diff --git a/wallet-sdk/src/androidMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.android.kt b/wallet-sdk/src/androidMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.android.kt index f10ff69a..72a8a97f 100644 --- a/wallet-sdk/src/androidMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.android.kt +++ b/wallet-sdk/src/androidMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.android.kt @@ -12,61 +12,72 @@ package com.algorand.common.algosdk +import cash.z.ecc.android.bip39.Mnemonics +import cash.z.ecc.android.bip39.Mnemonics.MnemonicCode +import cash.z.ecc.android.bip39.toSeed +import com.algorand.algosdk.account.Account +import com.algorand.algosdk.crypto.Address import com.algorand.common.algosdk.model.Algo25Account import com.algorand.common.algosdk.model.Bip39Account +import foundation.algorand.xhdwalletapi.KeyContext +import foundation.algorand.xhdwalletapi.XHDWalletAPIAndroid +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.security.Security import kotlin.random.Random + actual interface AlgoAccountSdk { actual fun createBip39Account(): Bip39Account - actual fun recoverBip39Account(mnemonic: String): Bip39Account + actual fun recoverBip39Account(mnemonic: String): Bip39Account? actual fun createAlgo25Account(): Algo25Account - actual fun recoverAlgo25Account(mnemonic: String): Algo25Account + actual fun recoverAlgo25Account(mnemonic: String): Algo25Account? } internal class AlgoAccountSdkImpl : AlgoAccountSdk { + init { + Security.removeProvider("BC") + Security.insertProviderAt(BouncyCastleProvider(), 0) + } override fun createBip39Account(): Bip39Account { - val accountAddress = generateRandomAddress() - val mnemonic = generate24WordMnemonic() - return Bip39Account(accountAddress, mnemonic, byteArrayOf()) + val generatedMnemonic = MnemonicCode(Mnemonics.WordCount.COUNT_24) + val wordsAsString = generatedMnemonic.joinToString(" ") + val accountAddress = generateBip39Address(generatedMnemonic) + return Bip39Account(accountAddress, wordsAsString, generatedMnemonic.toSeed()) } override fun recoverBip39Account(mnemonic: String): Bip39Account { - return Bip39Account(mnemonic, mnemonic, byteArrayOf()) + val m = MnemonicCode(mnemonic) + val accountAddress = generateBip39Address(m) + return Bip39Account(accountAddress, mnemonic, m.toSeed()) } override fun createAlgo25Account(): Algo25Account { - val accountAddress = generateRandomAddress() - val mnemonic = generate25WordMnemonic() - return Algo25Account(accountAddress, mnemonic, byteArrayOf()) - } - - override fun recoverAlgo25Account(mnemonic: String): Algo25Account { - return Algo25Account(mnemonic, mnemonic, byteArrayOf()) - } - - private fun generateRandomAddress(): String { - val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" // Base32 characters - val addressLength = 58 - - return (1..addressLength) - .map { alphabet[Random.nextInt(alphabet.length)] } - .joinToString("") + val account = Account() + return Algo25Account(account.address.toString(), account.toMnemonic(), account.toSeed()) } - private fun generate24WordMnemonic(): String { - return """ - Lorem ipsum dolor sit amet consectetur adipiscing elit - Mauris ornare orci et facilisis condimentum - Nunc imperdiet ultricies mi nec mattis erat In volutpat tempus - """.trimIndent().lowercase() + override fun recoverAlgo25Account(mnemonic: String): Algo25Account? { + try { + val account = Account(mnemonic) + return Algo25Account(account.address.toString(), account.toMnemonic(), account.toSeed()) + } catch (e: Exception) { + return null + } } - private fun generate25WordMnemonic(): String { - return """ - Lorem ipsum dolor sit amet consectetur adipiscing elit - Mauris ornare orci et facilisis condimentum - Nunc imperdiet ultricies mi nec mattis erat In volutpat tempus tortor - """.trimIndent().lowercase() + private fun generateBip39Address(mnemonic: MnemonicCode): String { + val xHDWalletAPI = XHDWalletAPIAndroid(mnemonic.toSeed()) + // Produce the PK and turn it into an Algorand formatted address + val algoAddress = + Address( + xHDWalletAPI.keyGen( + KeyContext.Address, + Random.nextInt().toUInt(), + Random.nextInt().toUInt(), + Random.nextInt().toUInt() + ) + ) + return algoAddress.toString() } } diff --git a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.kt b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.kt index ff84a994..0c7bb57b 100644 --- a/wallet-sdk/src/commonMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.kt +++ b/wallet-sdk/src/commonMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.kt @@ -19,9 +19,9 @@ expect interface AlgoAccountSdk { fun createBip39Account(): Bip39Account - fun recoverBip39Account(mnemonic: String): Bip39Account + fun recoverBip39Account(mnemonic: String): Bip39Account? fun createAlgo25Account(): Algo25Account - fun recoverAlgo25Account(mnemonic: String): Algo25Account + fun recoverAlgo25Account(mnemonic: String): Algo25Account? } diff --git a/wallet-sdk/src/iosMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.ios.kt b/wallet-sdk/src/iosMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.ios.kt index f10ff69a..e4f8f057 100644 --- a/wallet-sdk/src/iosMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.ios.kt +++ b/wallet-sdk/src/iosMain/kotlin/com/algorand/common/algosdk/AlgoAccountSdk.ios.kt @@ -18,9 +18,9 @@ import kotlin.random.Random actual interface AlgoAccountSdk { actual fun createBip39Account(): Bip39Account - actual fun recoverBip39Account(mnemonic: String): Bip39Account + actual fun recoverBip39Account(mnemonic: String): Bip39Account? actual fun createAlgo25Account(): Algo25Account - actual fun recoverAlgo25Account(mnemonic: String): Algo25Account + actual fun recoverAlgo25Account(mnemonic: String): Algo25Account? } internal class AlgoAccountSdkImpl : AlgoAccountSdk {