Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 0 additions & 179 deletions .claude/commands/pr.md

This file was deleted.

1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ suspend fun getData(): Result<Data> = 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

Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ android {
applicationId = "to.bitkit"
minSdk = 28
targetSdk = 36
versionCode = 162
versionName = "0.0.17"
versionCode = 163
versionName = "0.0.18"
testInstrumentationRunner = "to.bitkit.test.HiltTestRunner"
vectorDrawables {
useSupportLibrary = true
Expand Down
Binary file removed app/libs/LDK-release.aar
Binary file not shown.
13 changes: 0 additions & 13 deletions app/src/main/java/to/bitkit/ext/ByteArray.kt
Original file line number Diff line number Diff line change
@@ -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)
}
51 changes: 6 additions & 45 deletions app/src/main/java/to/bitkit/services/RNBackupClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@ 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.di.json
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
Expand Down Expand Up @@ -275,44 +273,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()

return runCatching {
val keysManager = KeysManager.of(bip32Seed, seconds, nanoSeconds)
val method = keysManager.javaClass.getMethod("get_node_secret_key")
when (val nodeSecretKey = method.invoke(keysManager)) {
is ByteArray -> nodeSecretKey
is List<*> -> nodeSecretKey.map { (it as UByte).toByte() }.toByteArray()
else -> throw ClassCastException("Unexpected type: ${nodeSecretKey?.javaClass?.name}")
}
}.getOrElse { bip32Seed }
}
private fun deriveSigningKey(mnemonic: String, passphrase: String?): ByteArray =
deriveNodeSecretFromMnemonic(mnemonic, passphrase).map { it.toByte() }.toByteArray()

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, 2048)
return (generator.generateDerivedParameters(512) as KeyParameter).key
}

private fun deriveMasterKey(seed: ByteArray): ByteArray {
val hmac = javax.crypto.Mac.getInstance("HmacSHA512")
val keySpec = javax.crypto.spec.SecretKeySpec("Bitcoin seed".toByteArray(), "HmacSHA512")
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) {
Expand All @@ -331,10 +296,6 @@ class RNBackupClient @Inject constructor(

return cipher.doFinal(ciphertext + tag)
}

private fun ByteArray.toHex(): String {
return this.joinToString("") { "%02x".format(it) }
}
}

@Serializable
Expand Down
12 changes: 6 additions & 6 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.33" }
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" }
Expand All @@ -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" }
Expand All @@ -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.3" } # 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" }
Expand Down
Loading