diff --git a/AGENTS.md b/AGENTS.md index e70f3c16a..ed2897729 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -206,6 +206,7 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - ALWAYS use template in `.github/pull_request_template.md` for PR descriptions - ALWAYS wrap `ULong` numbers with `USat` in arithmetic operations, to guard against overflows - PREFER to use one-liners with `run { }` when applicable, e.g. `override fun someCall(value: String) = run { this.value = value }` +- ALWAYS add imports instead of inline fully-qualified names ### Architecture Guidelines diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4450eab5b..7a4c66e35 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -46,8 +46,8 @@ android { applicationId = "to.bitkit" minSdk = 28 targetSdk = 36 - versionCode = 162 - versionName = "0.0.17" + versionCode = 163 + versionName = "2.0.0-rc.1" testInstrumentationRunner = "to.bitkit.test.HiltTestRunner" vectorDrawables { useSupportLibrary = true @@ -176,8 +176,7 @@ android { } composeCompiler { - featureFlags = setOf( - ) + featureFlags = setOf() reportsDestination = layout.buildDirectory.dir("compose_compiler") } diff --git a/app/libs/LDK-release.aar b/app/libs/LDK-release.aar deleted file mode 100644 index e0b883249..000000000 Binary files a/app/libs/LDK-release.aar and /dev/null differ diff --git a/app/src/main/java/to/bitkit/ext/ByteArray.kt b/app/src/main/java/to/bitkit/ext/ByteArray.kt index 7aa91d0d5..af0aad5b4 100644 --- a/app/src/main/java/to/bitkit/ext/ByteArray.kt +++ b/app/src/main/java/to/bitkit/ext/ByteArray.kt @@ -1,26 +1,13 @@ package to.bitkit.ext -import java.security.MessageDigest import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi -// region hex -@OptIn(ExperimentalStdlibApi::class) fun ByteArray.toHex(): String = this.toHexString() -@OptIn(ExperimentalStdlibApi::class) fun String.fromHex(): ByteArray = this.hexToByteArray() -// endregion -@OptIn(ExperimentalEncodingApi::class) fun ByteArray.toBase64(): String = Base64.encode(this) -@OptIn(ExperimentalEncodingApi::class) fun String.fromBase64(): ByteArray = Base64.decode(this) val String.uByteList get() = this.toByteArray().map { it.toUByte() } - -fun ByteArray.toSha256(): ByteArray { - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(this) -} diff --git a/app/src/main/java/to/bitkit/services/RNBackupClient.kt b/app/src/main/java/to/bitkit/services/RNBackupClient.kt index e2d87742b..61160c3ab 100644 --- a/app/src/main/java/to/bitkit/services/RNBackupClient.kt +++ b/app/src/main/java/to/bitkit/services/RNBackupClient.kt @@ -17,14 +17,12 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put -import org.bouncycastle.crypto.digests.SHA512Digest -import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator -import org.bouncycastle.crypto.params.KeyParameter -import org.ldk.structs.KeysManager import org.lightningdevkit.ldknode.Network +import org.lightningdevkit.ldknode.deriveNodeSecretFromMnemonic import to.bitkit.data.keychain.Keychain import to.bitkit.di.IoDispatcher import to.bitkit.env.Env +import to.bitkit.ext.toHex import to.bitkit.utils.AppError import to.bitkit.utils.Crypto import to.bitkit.utils.Logger @@ -255,41 +253,11 @@ class RNBackupClient @Inject constructor( return crypto.sign(fullMessage, privateKey) } - private fun deriveSigningKey(mnemonic: String, passphrase: String?): ByteArray { - val bip39Seed = deriveSeed(mnemonic, passphrase) - val bip32Seed = deriveMasterKey(bip39Seed) - val seconds = System.currentTimeMillis() / 1000L - val nanoSeconds = ((System.currentTimeMillis() % 1000) * 1_000_000).toInt() + private fun deriveSigningKey(mnemonic: String, passphrase: String?): ByteArray = + deriveNodeSecretFromMnemonic(mnemonic, passphrase).map { it.toByte() }.toByteArray() - return runCatching { - val keysManager = KeysManager.of(bip32Seed, seconds, nanoSeconds) - keysManager._node_secret_key - }.getOrElse { bip32Seed } - } - - private fun deriveEncryptionKey(mnemonic: String, passphrase: String?): ByteArray { - // Match iOS: use the same node secret key as signing key for encryption - // iOS uses SymmetricKey(data: secretKey) where secretKey is the node secret key - return deriveSigningKey(mnemonic, passphrase) - } - - private fun deriveSeed(mnemonic: String, passphrase: String?): ByteArray { - val mnemonicBytes = mnemonic.toByteArray(Charsets.UTF_8) - val salt = ("mnemonic" + (passphrase ?: "")).toByteArray(Charsets.UTF_8) - val generator = PKCS5S2ParametersGenerator(SHA512Digest()) - generator.init(mnemonicBytes, salt, PBKDF2_ITERATIONS) - - return (generator.generateDerivedParameters(PBKDF2_KEY_LENGTH_BITS) as KeyParameter).key - } - - private fun deriveMasterKey(seed: ByteArray): ByteArray { - val algorithm = "HmacSHA512" - val hmac = Mac.getInstance(algorithm) - val keySpec = SecretKeySpec("Bitcoin seed".toByteArray(), algorithm) - hmac.init(keySpec) - val i = hmac.doFinal(seed) - return i.sliceArray(0 until 32) - } + private fun deriveEncryptionKey(mnemonic: String, passphrase: String?): ByteArray = + deriveSigningKey(mnemonic, passphrase) private fun decrypt(blob: ByteArray, encryptionKey: ByteArray): ByteArray { if (blob.size < GCM_IV_LENGTH + GCM_TAG_LENGTH) throw RNBackupError.DecryptFailed("Data too short") @@ -306,8 +274,6 @@ class RNBackupClient @Inject constructor( return cipher.doFinal(ciphertext + tag) } - - private fun ByteArray.toHex(): String = this.joinToString("") { "%02x".format(it) } } @Serializable diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 95a33d23b..877e9e053 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,17 +15,17 @@ haze = "1.7.1" [libraries] accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version = "0.36.0" } accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version = "0.37.3" } -activity-compose = { module = "androidx.activity:activity-compose", version = "1.12.1" } +activity-compose = { module = "androidx.activity:activity-compose", version = "1.12.2" } appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.1" } barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version = "17.3.0" } -biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha04" } +biometric = { module = "androidx.biometric:biometric", version = "1.4.0-alpha05" } bitkit-core = { module = "com.synonym:bitkit-core-android", version = "0.1.35" } bouncycastle-provider-jdk = { module = "org.bouncycastle:bcprov-jdk18on", version = "1.83" } camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } camera-view = { module = "androidx.camera:camera-view", version.ref = "camera" } # https://developer.android.com/develop/ui/compose/bom/bom-mapping -compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2025.12.00" } +compose-bom = { group = "androidx.compose", name = "compose-bom", version = "2025.12.01" } compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } compose-material3 = { module = "androidx.compose.material3:material3" } compose-ui = { group = "androidx.compose.ui", name = "ui" } @@ -39,8 +39,8 @@ core-ktx = { module = "androidx.core:core-ktx", version = "1.17.0" } core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version = "1.2.0" } datastore-preferences = { module = "androidx.datastore:datastore-preferences", version = "1.2.0" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } -detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.5.1" } -firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.6.0" } +detekt-compose-rules = { module = "io.nlopez.compose.rules:detekt", version = "0.5.3" } +firebase-bom = { module = "com.google.firebase:firebase-bom", version = "34.7.0" } firebase-messaging = { module = "com.google.firebase:firebase-messaging" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } @@ -57,7 +57,7 @@ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } -ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.7.0-rc.2" } # fork | local: remove `v` +ldk-node-android = { module = "com.github.synonymdev:ldk-node", version = "v0.7.0-rc.6" } # fork | local: remove `v` lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" } lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }