Skip to content

Commit 6473f3f

Browse files
ovitrifclaude
andcommitted
fix: restore RNBackupClient after rebase
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 369d80f commit 6473f3f

1 file changed

Lines changed: 27 additions & 91 deletions

File tree

app/src/main/java/to/bitkit/services/RNBackupClient.kt

Lines changed: 27 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,12 @@ import kotlinx.serialization.Serializable
1717
import kotlinx.serialization.json.Json
1818
import kotlinx.serialization.json.buildJsonObject
1919
import kotlinx.serialization.json.put
20-
import org.bouncycastle.crypto.digests.SHA512Digest
21-
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator
22-
import org.bouncycastle.crypto.params.KeyParameter
23-
import org.ldk.structs.KeysManager
2420
import org.lightningdevkit.ldknode.Network
21+
import org.lightningdevkit.ldknode.deriveNodeSecretFromMnemonic
2522
import to.bitkit.data.keychain.Keychain
2623
import to.bitkit.di.IoDispatcher
27-
import to.bitkit.di.json
2824
import to.bitkit.env.Env
25+
import to.bitkit.ext.toHex
2926
import to.bitkit.utils.AppError
3027
import to.bitkit.utils.Crypto
3128
import to.bitkit.utils.Logger
@@ -43,6 +40,7 @@ class RNBackupClient @Inject constructor(
4340
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
4441
private val json: Json,
4542
) {
43+
@Suppress("SpellCheckingInspection")
4644
companion object {
4745
private const val TAG = "RNBackup"
4846
private const val VERSION = "v1"
@@ -58,8 +56,7 @@ class RNBackupClient @Inject constructor(
5856

5957
suspend fun listFiles(fileGroup: String? = "ldk"): RNBackupListResponse? = withContext(ioDispatcher) {
6058
runCatching {
61-
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)
62-
?: throw RNBackupError.NotSetup()
59+
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw RNBackupError.NotSetup()
6360
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)
6461

6562
val bearer = authenticate(mnemonic, passphrase)
@@ -68,9 +65,7 @@ class RNBackupClient @Inject constructor(
6865
header("Authorization", bearer.bearer)
6966
}
7067

71-
if (!response.status.isSuccess()) {
72-
throw RNBackupError.RequestFailed("Status: ${response.status.value}")
73-
}
68+
if (!response.status.isSuccess()) throw RNBackupError.RequestFailed("Status: ${response.status.value}")
7469

7570
response.body<RNBackupListResponse>()
7671
}.onFailure { e ->
@@ -80,8 +75,7 @@ class RNBackupClient @Inject constructor(
8075

8176
suspend fun retrieve(label: String, fileGroup: String? = null): ByteArray? = withContext(ioDispatcher) {
8277
runCatching {
83-
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)
84-
?: throw RNBackupError.NotSetup()
78+
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw RNBackupError.NotSetup()
8579
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)
8680

8781
val bearer = authenticate(mnemonic, passphrase)
@@ -90,14 +84,13 @@ class RNBackupClient @Inject constructor(
9084
header("Authorization", bearer.bearer)
9185
}
9286

93-
if (!response.status.isSuccess()) {
94-
throw RNBackupError.RequestFailed("Status: ${response.status.value}")
95-
}
87+
if (!response.status.isSuccess()) throw RNBackupError.RequestFailed("Status: ${response.status.value}")
9688

9789
val encryptedData = response.body<ByteArray>()
9890
if (encryptedData.isEmpty()) throw RNBackupError.RequestFailed("Retrieved data is empty")
9991

10092
val encryptionKey = deriveEncryptionKey(mnemonic, passphrase)
93+
10194
decrypt(encryptedData, encryptionKey).also {
10295
if (it.isEmpty()) throw RNBackupError.DecryptFailed("Decrypted data is empty")
10396
}
@@ -108,8 +101,7 @@ class RNBackupClient @Inject constructor(
108101

109102
suspend fun retrieveChannelMonitor(channelId: String): ByteArray? = withContext(ioDispatcher) {
110103
runCatching {
111-
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)
112-
?: throw RNBackupError.NotSetup()
104+
val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) ?: throw RNBackupError.NotSetup()
113105
val passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name)
114106

115107
val bearer = authenticate(mnemonic, passphrase)
@@ -124,9 +116,7 @@ class RNBackupClient @Inject constructor(
124116
header("Authorization", bearer.bearer)
125117
}
126118

127-
if (!response.status.isSuccess()) {
128-
throw RNBackupError.RequestFailed("Status: ${response.status.value}")
129-
}
119+
if (!response.status.isSuccess()) throw RNBackupError.RequestFailed("Status: ${response.status.value}")
130120

131121
val encryptedData = response.body<ByteArray>()
132122
if (encryptedData.isEmpty()) throw RNBackupError.RequestFailed("Retrieved data is empty")
@@ -140,18 +130,6 @@ class RNBackupClient @Inject constructor(
140130
}.getOrNull()
141131
}
142132

143-
suspend fun hasBackup(): Boolean = withContext(ioDispatcher) {
144-
runCatching {
145-
val ldkFiles = listFiles(fileGroup = "ldk")
146-
val bitkitFiles = listFiles(fileGroup = "bitkit")
147-
val hasLdkFiles = !ldkFiles?.list.isNullOrEmpty() || !ldkFiles?.channelMonitors.isNullOrEmpty()
148-
val hasBitkitFiles = !bitkitFiles?.list.isNullOrEmpty()
149-
hasLdkFiles || hasBitkitFiles
150-
}.onFailure { e ->
151-
Logger.error("Failed to check if backup exists", e, context = TAG)
152-
}.getOrDefault(false)
153-
}
154-
155133
suspend fun getLatestBackupTimestamp(): ULong? = withContext(ioDispatcher) {
156134
runCatching {
157135
val bitkitFiles = listFiles(fileGroup = "bitkit")?.list ?: return@withContext null
@@ -174,15 +152,16 @@ class RNBackupClient @Inject constructor(
174152

175153
var latestTimestamp: ULong? = null
176154
for (label in labels) {
177-
if ("$label.bin" !in bitkitFiles) continue
178-
179-
val data = retrieve(label, fileGroup = "bitkit") ?: continue
180-
val timestamp = runCatching {
181-
json.decodeFromString<BackupWithMetadata>(String(data)).metadata?.timestamp
182-
}.getOrNull() ?: continue
183-
184-
val ts = (timestamp / 1000).toULong()
185-
latestTimestamp = maxOf(latestTimestamp ?: 0uL, ts)
155+
if ("$label.bin" in bitkitFiles) {
156+
retrieve(label, fileGroup = "bitkit")?.let { data ->
157+
runCatching {
158+
json.decodeFromString<BackupWithMetadata>(String(data)).metadata?.timestamp
159+
}.getOrNull()?.let { timestamp ->
160+
val ts = (timestamp / 1000).toULong()
161+
latestTimestamp = maxOf(latestTimestamp ?: 0uL, ts)
162+
}
163+
}
164+
}
186165
}
187166
latestTimestamp
188167
}.onFailure { e ->
@@ -235,9 +214,7 @@ class RNBackupClient @Inject constructor(
235214
setBody(challengeBody)
236215
}
237216

238-
if (!challengeResponse.status.isSuccess()) {
239-
throw RNBackupError.AuthFailed()
240-
}
217+
if (!challengeResponse.status.isSuccess()) throw RNBackupError.AuthFailed()
241218

242219
val challengeResult = challengeResponse.body<AuthChallengeResponse>()
243220
val authBody = json.encodeToString(
@@ -253,9 +230,7 @@ class RNBackupClient @Inject constructor(
253230
setBody(authBody)
254231
}
255232

256-
if (!authResponse.status.isSuccess()) {
257-
throw RNBackupError.AuthFailed()
258-
}
233+
if (!authResponse.status.isSuccess()) throw RNBackupError.AuthFailed()
259234

260235
authResponse.body<AuthBearerResponse>().also { cachedBearer = it }
261236
}
@@ -275,49 +250,14 @@ class RNBackupClient @Inject constructor(
275250
return crypto.sign(fullMessage, privateKey)
276251
}
277252

278-
private fun deriveSigningKey(mnemonic: String, passphrase: String?): ByteArray {
279-
val bip39Seed = deriveSeed(mnemonic, passphrase)
280-
val bip32Seed = deriveMasterKey(bip39Seed)
281-
val seconds = System.currentTimeMillis() / 1000L
282-
val nanoSeconds = ((System.currentTimeMillis() % 1000) * 1_000_000).toInt()
283-
284-
return runCatching {
285-
val keysManager = KeysManager.of(bip32Seed, seconds, nanoSeconds)
286-
val method = keysManager.javaClass.getMethod("get_node_secret_key")
287-
when (val nodeSecretKey = method.invoke(keysManager)) {
288-
is ByteArray -> nodeSecretKey
289-
is List<*> -> nodeSecretKey.map { (it as UByte).toByte() }.toByteArray()
290-
else -> throw ClassCastException("Unexpected type: ${nodeSecretKey?.javaClass?.name}")
291-
}
292-
}.getOrElse { bip32Seed }
293-
}
294-
295-
private fun deriveEncryptionKey(mnemonic: String, passphrase: String?): ByteArray {
296-
// Match iOS: use the same node secret key as signing key for encryption
297-
// iOS uses SymmetricKey(data: secretKey) where secretKey is the node secret key
298-
return deriveSigningKey(mnemonic, passphrase)
299-
}
253+
private fun deriveSigningKey(mnemonic: String, passphrase: String?): ByteArray =
254+
deriveNodeSecretFromMnemonic(mnemonic, passphrase).map { it.toByte() }.toByteArray()
300255

301-
private fun deriveSeed(mnemonic: String, passphrase: String?): ByteArray {
302-
val mnemonicBytes = mnemonic.toByteArray(Charsets.UTF_8)
303-
val salt = ("mnemonic" + (passphrase ?: "")).toByteArray(Charsets.UTF_8)
304-
val generator = PKCS5S2ParametersGenerator(SHA512Digest())
305-
generator.init(mnemonicBytes, salt, 2048)
306-
return (generator.generateDerivedParameters(512) as KeyParameter).key
307-
}
308-
309-
private fun deriveMasterKey(seed: ByteArray): ByteArray {
310-
val hmac = javax.crypto.Mac.getInstance("HmacSHA512")
311-
val keySpec = javax.crypto.spec.SecretKeySpec("Bitcoin seed".toByteArray(), "HmacSHA512")
312-
hmac.init(keySpec)
313-
val i = hmac.doFinal(seed)
314-
return i.sliceArray(0 until 32)
315-
}
256+
private fun deriveEncryptionKey(mnemonic: String, passphrase: String?): ByteArray =
257+
deriveSigningKey(mnemonic, passphrase)
316258

317259
private fun decrypt(blob: ByteArray, encryptionKey: ByteArray): ByteArray {
318-
if (blob.size < GCM_IV_LENGTH + GCM_TAG_LENGTH) {
319-
throw RNBackupError.DecryptFailed("Data too short")
320-
}
260+
if (blob.size < GCM_IV_LENGTH + GCM_TAG_LENGTH) throw RNBackupError.DecryptFailed("Data too short")
321261

322262
val nonce = blob.sliceArray(0 until GCM_IV_LENGTH)
323263
val tag = blob.sliceArray(blob.size - GCM_TAG_LENGTH until blob.size)
@@ -331,10 +271,6 @@ class RNBackupClient @Inject constructor(
331271

332272
return cipher.doFinal(ciphertext + tag)
333273
}
334-
335-
private fun ByteArray.toHex(): String {
336-
return this.joinToString("") { "%02x".format(it) }
337-
}
338274
}
339275

340276
@Serializable

0 commit comments

Comments
 (0)