@@ -17,15 +17,12 @@ import kotlinx.serialization.Serializable
1717import kotlinx.serialization.json.Json
1818import kotlinx.serialization.json.buildJsonObject
1919import 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
2420import org.lightningdevkit.ldknode.Network
21+ import org.lightningdevkit.ldknode.deriveNodeSecretFromMnemonic
2522import to.bitkit.data.keychain.Keychain
2623import to.bitkit.di.IoDispatcher
27- import to.bitkit.di.json
2824import to.bitkit.env.Env
25+ import to.bitkit.ext.toHex
2926import to.bitkit.utils.AppError
3027import to.bitkit.utils.Crypto
3128import 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