diff --git a/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt b/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt index 654fa81b..9d1b4a3c 100644 --- a/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt +++ b/lib/android/src/main/java/com/reactnativeldk/LdkModule.kt @@ -8,12 +8,16 @@ import org.ldk.batteries.ChannelManagerConstructor import org.ldk.batteries.NioPeerHandler import org.ldk.enums.Currency import org.ldk.enums.Network +import org.ldk.enums.Recipient +import org.ldk.impl.bindings.LDKDestination.Node import org.ldk.impl.bindings.get_ldk_c_bindings_version import org.ldk.impl.bindings.get_ldk_version import org.ldk.structs.* import org.ldk.structs.Result_Bolt11InvoiceParseOrSemanticErrorZ.Result_Bolt11InvoiceParseOrSemanticErrorZ_OK import org.ldk.structs.Result_Bolt11InvoiceSignOrCreationErrorZ.Result_Bolt11InvoiceSignOrCreationErrorZ_OK import org.ldk.structs.Result_PaymentIdPaymentErrorZ.Result_PaymentIdPaymentErrorZ_OK +import org.ldk.structs.Result_PublicKeyErrorZ.Result_PublicKeyErrorZ_OK +import org.ldk.structs.Result_PublicKeyNoneZ.Result_PublicKeyNoneZ_OK import org.ldk.structs.Result_StringErrorZ.Result_StringErrorZ_OK import org.ldk.util.UInt128 import java.io.File @@ -89,7 +93,7 @@ enum class LdkErrors { file_does_not_exist, data_too_large_for_rn, backup_setup_required, - backup_setup_check_failed, + backup_check_failed, backup_setup_failed, backup_restore_failed, backup_restore_failed_existing_files @@ -119,7 +123,8 @@ enum class LdkCallbackResponses { close_channel_success, file_write_success, backup_client_setup_success, - backup_restore_success + backup_restore_success, + backup_client_check_success } enum class LdkFileNames(val fileName: String) { @@ -223,67 +228,6 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod handleResolve(promise, LdkCallbackResponses.log_write_success) } - @ReactMethod - fun backupSetup(seed: String, network: String, server: String, token: String, promise: Promise) { - try { - BackupClient.skipRemoteBackup = false - BackupClient.setup(seed.hexa(), network, server, token) - - val ping = "ping${Random().nextInt(1000)}" - BackupClient.persist(BackupClient.Label.PING(), ping.toByteArray()) - val pingRetrieved = BackupClient.retrieve(BackupClient.Label.PING()) - if (pingRetrieved.toString(Charsets.UTF_8) != ping) { - return handleReject(promise, LdkErrors.backup_setup_check_failed) - } - - BackupClient.retrieveCompleteBackup() - - handleResolve(promise, LdkCallbackResponses.backup_client_setup_success) - } catch (e: Exception) { - return handleReject(promise, LdkErrors.backup_setup_failed, Error(e)) - } - } - - @ReactMethod - fun restoreFromRemoteBackup(overwrite: Boolean, promise: Promise) { - if (BackupClient.requiresSetup()) { - return handleReject(promise, LdkErrors.backup_setup_required) - } - - if (accountStoragePath == "") { - return handleReject(promise, LdkErrors.init_storage_path) - } - if (channelStoragePath == "") { - return handleReject(promise, LdkErrors.init_storage_path) - } - - try { - if (!overwrite) { - val accountStoragePath = File(accountStoragePath) - val channelStoragePath = File(channelStoragePath) - - if (accountStoragePath.exists() || channelStoragePath.exists()) { - return handleReject(promise, LdkErrors.backup_restore_failed_existing_files) - } - } - - val completeBackup = BackupClient.retrieveCompleteBackup() - for (file in completeBackup.files) { - val newFile = File(accountStoragePath + "/" + file.key) - newFile.writeBytes(file.value) - } - - for (channelFile in completeBackup.channelFiles) { - val newFile = File(channelStoragePath + "/" + channelFile.key) - newFile.writeBytes(channelFile.value) - } - - handleResolve(promise, LdkCallbackResponses.backup_restore_success) - } catch (e: Exception) { - return handleReject(promise, LdkErrors.backup_restore_failed, Error(e)) - } - } - @ReactMethod fun initChainMonitor(promise: Promise) { if (chainMonitor !== null) { @@ -304,7 +248,7 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod @ReactMethod fun initKeysManager(seed: String, promise: Promise) { if (keysManager !== null) { - return handleReject(promise, LdkErrors.already_init) + return handleResolve(promise, LdkCallbackResponses.keys_manager_init_success) } val nanoSeconds = System.currentTimeMillis() * 1000 @@ -1177,6 +1121,95 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod promise.resolve((res as Result_StringErrorZ_OK).res) } + + //Backup methods + @ReactMethod + fun backupSetup(seed: String, network: String, server: String, serverPubKey: String, promise: Promise) { + val seedBytes = seed.hexa() + if (keysManager == null) { + val nanoSeconds = System.currentTimeMillis() * 1000 + val seconds = nanoSeconds / 1000 / 1000 + if (seedBytes.count() != 32) { + return handleReject(promise, LdkErrors.invalid_seed_hex) + } + + keysManager = KeysManager.of(seedBytes, seconds, nanoSeconds.toInt()) + } + + val pubKeyRes = keysManager!!.as_NodeSigner().get_node_id(Recipient.LDKRecipient_Node) + if (!pubKeyRes.is_ok) { + return handleReject(promise, LdkErrors.backup_setup_failed) + } + + try { + BackupClient.skipRemoteBackup = false + BackupClient.setup( + keysManager!!._node_secret_key, + (pubKeyRes as Result_PublicKeyNoneZ_OK).res, + network, + server, + serverPubKey + ) + + handleResolve(promise, LdkCallbackResponses.backup_client_setup_success) + } catch (e: Exception) { + return handleReject(promise, LdkErrors.backup_setup_failed, Error(e)) + } + } + + @ReactMethod + fun restoreFromRemoteBackup(overwrite: Boolean, promise: Promise) { + if (BackupClient.requiresSetup) { + return handleReject(promise, LdkErrors.backup_setup_required) + } + + if (accountStoragePath == "") { + return handleReject(promise, LdkErrors.init_storage_path) + } + if (channelStoragePath == "") { + return handleReject(promise, LdkErrors.init_storage_path) + } + + try { + if (!overwrite) { + val accountStoragePath = File(accountStoragePath) + val channelStoragePath = File(channelStoragePath) + + if (accountStoragePath.exists() || channelStoragePath.exists()) { + return handleReject(promise, LdkErrors.backup_restore_failed_existing_files) + } + } + + val completeBackup = BackupClient.retrieveCompleteBackup() + for (file in completeBackup.files) { + val newFile = File(accountStoragePath + "/" + file.key) + newFile.writeBytes(file.value) + } + + for (channelFile in completeBackup.channelFiles) { + val newFile = File(channelStoragePath + "/" + channelFile.key) + newFile.writeBytes(channelFile.value) + } + + handleResolve(promise, LdkCallbackResponses.backup_restore_success) + } catch (e: Exception) { + return handleReject(promise, LdkErrors.backup_restore_failed, Error(e)) + } + } + + @ReactMethod + fun backupSelfCheck(promise: Promise) { + if (BackupClient.requiresSetup) { + return handleReject(promise, LdkErrors.backup_setup_required) + } + + try { + BackupClient.selfCheck() + handleResolve(promise, LdkCallbackResponses.backup_client_check_success) + } catch (e: Exception) { + handleReject(promise, LdkErrors.backup_check_failed, Error(e)) + } + } } object LdkEventEmitter { diff --git a/lib/android/src/main/java/com/reactnativeldk/classes/BackupClient.kt b/lib/android/src/main/java/com/reactnativeldk/classes/BackupClient.kt index 3ddcc0e1..33f5006d 100644 --- a/lib/android/src/main/java/com/reactnativeldk/classes/BackupClient.kt +++ b/lib/android/src/main/java/com/reactnativeldk/classes/BackupClient.kt @@ -1,12 +1,18 @@ package com.reactnativeldk.classes import com.reactnativeldk.EventTypes +import com.reactnativeldk.LdkErrors import com.reactnativeldk.LdkEventEmitter +import com.reactnativeldk.handleReject import com.reactnativeldk.hexEncodedString import com.reactnativeldk.hexa import org.json.JSONObject +import org.ldk.structs.Result_StringErrorZ.Result_StringErrorZ_OK +import org.ldk.structs.UtilMethods import java.net.HttpURLConnection import java.net.URL +import java.security.MessageDigest import java.security.SecureRandom +import java.util.Random import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.IvParameterSpec @@ -18,6 +24,8 @@ class BackupError : Exception() { val missingBackup = MissingBackup() val invalidServerResponse = InvalidServerResponse(0) val decryptFailed = DecryptFailed("") + val signingError = SigningError() + val serverChallengeResponseFailed = ServerChallengeResponseFailed() } } @@ -26,6 +34,8 @@ class RequiresSetup() : Exception("BackupClient requires setup") class MissingBackup() : Exception("Retrieve failed. Missing backup.") class InvalidServerResponse(code: Int) : Exception("Invalid backup server response ($code)") class DecryptFailed(msg: String) : Exception("Failed to decrypt backup payload. $msg") +class SigningError() : Exception("Failed to sign message") +class ServerChallengeResponseFailed() : Exception("Server challenge response failed") class CompleteBackup( val files: Map, @@ -47,33 +57,52 @@ class BackupClient { enum class Method(val value: String) { PERSIST("persist"), RETRIEVE("retrieve"), - LIST("list") + LIST("list"), + AUTH_CHALLENGE("auth/challenge"), + AUTH_RESPONSE("auth/response") } + private var version = "v1" + private var signedMessagePrefix = "react-native-ldk backup server auth:" + var skipRemoteBackup = true //Allow dev to opt out (for development), will not throw error when attempting to persist - private val version = "v1" - var network: String? = null - var server: String? = null - var encryptionKey: SecretKeySpec? = null - var token: String? = null + private var network: String? = null + private var server: String? = null + private var serverPubKey: String? = null + private var secretKey: ByteArray? = null + private val encryptionKey: SecretKeySpec? + get() = if (secretKey != null) { + SecretKeySpec(secretKey, "AES") + } else { + null + } + private var pubKey: ByteArray? = null - var requiresSetup = { - server == null - } + val requiresSetup: Boolean + get() = server == null - fun setup(seed: ByteArray, network: String, server: String, token: String) { + fun setup(secretKey: ByteArray, pubKey: ByteArray, network: String, server: String, serverPubKey: String) { + this.secretKey = secretKey + this.pubKey = pubKey this.network = network this.server = server - this.encryptionKey = SecretKeySpec(seed, "AES") - this.token = token + this.serverPubKey = serverPubKey + + LdkEventEmitter.send( + EventTypes.native_log, + "BackupClient setup for synchronous remote persistence. Server: $server" + ) + + if (requiresSetup) { + throw RuntimeException(this.server) + } } @Throws(BackupError::class) private fun backupUrl( method: Method, - label: Label? = null, - channelId: String = "" + label: Label? = null ): URL { val network = network ?: throw BackupError.requiresSetup val server = server ?: throw BackupError.requiresSetup @@ -99,19 +128,20 @@ class BackupClient { val cipher = Cipher.getInstance("AES/GCM/NoPadding") val random = SecureRandom() - val iv = ByteArray(12) + val nonce = ByteArray(12) + random.nextBytes(nonce) - random.nextBytes(iv) - - val gcmParameterSpec = GCMParameterSpec(128, iv) + val gcmParameterSpec = GCMParameterSpec(128, nonce) cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, gcmParameterSpec) + val cipherBytes = cipher.doFinal(blob) + return nonce + cipherBytes + } - // Encrypt the plain text - val cipherText = cipher.doFinal(blob) - - // Return the IV concatenated with the cipher text - return iv + cipherText + private fun hash(blob: ByteArray): String { + val messageDigest = MessageDigest.getInstance("SHA-256") + val hash = messageDigest.digest(blob) + return hash.joinToString("") { String.format("%02x", it) } } private fun decrypt(blob: ByteArray): ByteArray { @@ -136,7 +166,16 @@ class BackupClient { return } + if (pubKey == null || serverPubKey == null) { + throw BackupError.requiresSetup + } + + val pubKeyHex = pubKey!!.hexEncodedString() val encryptedBackup = encrypt(bytes) + val signedHash = sign(hash(encryptedBackup)) + //Hash of pubKey+timestamp + val clientChallenge = hash("$pubKeyHex${System.currentTimeMillis()}".toByteArray(Charsets.UTF_8)) + val url = backupUrl(Method.PERSIST, label) LdkEventEmitter.send( @@ -148,13 +187,34 @@ class BackupClient { urlConnection.requestMethod = "POST" urlConnection.doOutput = true urlConnection.setRequestProperty("Content-Type", "application/octet-stream") - urlConnection.setRequestProperty("Authorization", token) + urlConnection.setRequestProperty("Signed-Hash", signedHash) + urlConnection.setRequestProperty("Public-Key", pubKeyHex) + urlConnection.setRequestProperty("Challenge", clientChallenge) val outputStream = urlConnection.outputStream outputStream.write(encryptedBackup) outputStream.close() - val responseCode = urlConnection.responseCode + if (urlConnection.responseCode != 200) { + LdkEventEmitter.send( + EventTypes.native_log, + "Remote persist failed for ${label.string} with response code ${urlConnection.responseCode}" + ) + + throw InvalidServerResponse(urlConnection.responseCode) + } + + //Verify signed response + val inputStream = urlConnection.inputStream + val jsonString = inputStream.bufferedReader().use { it.readText() } + inputStream.close() + + val signature = JSONObject(jsonString).getString("signature") + + if (!verifySignature(clientChallenge, signature, serverPubKey!!)) { + throw BackupError.serverChallengeResponseFailed + } + LdkEventEmitter.send( EventTypes.native_log, "Remote persist success for ${label.string}" @@ -163,6 +223,7 @@ class BackupClient { @Throws(BackupError::class) fun retrieve(label: Label): ByteArray { + val bearer = "TODO" val url = backupUrl(Method.RETRIEVE, label) LdkEventEmitter.send( @@ -173,7 +234,7 @@ class BackupClient { val urlConnection = url.openConnection() as HttpURLConnection urlConnection.requestMethod = "GET" urlConnection.setRequestProperty("Content-Type", "application/octet-stream") - urlConnection.setRequestProperty("Authorization", token) + urlConnection.setRequestProperty("Authorization", bearer) val responseCode = urlConnection.responseCode @@ -206,6 +267,8 @@ class BackupClient { @Throws(BackupError::class) fun retrieveCompleteBackup(): CompleteBackup { + val bearer = "TODO" + val url = backupUrl(Method.LIST) LdkEventEmitter.send( @@ -216,7 +279,7 @@ class BackupClient { val urlConnection = url.openConnection() as HttpURLConnection urlConnection.requestMethod = "GET" urlConnection.setRequestProperty("Content-Type", "application/json") - urlConnection.setRequestProperty("Authorization", token) + urlConnection.setRequestProperty("Authorization", bearer) val responseCode = urlConnection.responseCode @@ -251,5 +314,38 @@ class BackupClient { return CompleteBackup(files = files, channelFiles = channelFiles) } + + @Throws(BackupError::class) + fun selfCheck() { + val ping = "ping${Random().nextInt(1000)}" + persist(Label.PING(), ping.toByteArray()) + + //TODO add check back +// val pingRetrieved = BackupClient.retrieve(BackupClient.Label.PING()) +// if (pingRetrieved.toString(Charsets.UTF_8) != ping) { +// +// } + } + + fun sign(message: String): String { + if (secretKey == null) { + throw BackupError.requiresSetup + } + + val res = UtilMethods.sign("$signedMessagePrefix$message".toByteArray(Charsets.UTF_8), secretKey) + if (!res.is_ok) { + throw BackupError.signingError + } + + return (res as Result_StringErrorZ_OK).res + } + + private fun verifySignature(message: String, signature: String, pubKey: String): Boolean { + return UtilMethods.verify( + "$signedMessagePrefix$message".toByteArray(Charsets.UTF_8), + signature, + pubKey.hexa() + ) + } } } diff --git a/lib/ios/Classes/BackupClient.swift b/lib/ios/Classes/BackupClient.swift index 0a838a05..caf60f58 100644 --- a/lib/ios/Classes/BackupClient.swift +++ b/lib/ios/Classes/BackupClient.swift @@ -54,8 +54,7 @@ struct BackupRetrieveBearer: Codable { } class BackupClient { - private static let version = "v1" - private static let signedMessagePrefix = "react-native-ldk backup server auth:" + enum Label { case ping @@ -79,7 +78,7 @@ class BackupClient { } } - enum Method: String { + private enum Method: String { case persist = "persist" case retrieve = "retrieve" case list = "list" @@ -87,21 +86,24 @@ class BackupClient { case authResponse = "auth/response" } + private static let version = "v1" + private static let signedMessagePrefix = "react-native-ldk backup server auth:" + static var skipRemoteBackup = true //Allow dev to opt out (for development), will not throw error when attempting to persist - static var network: String? - static var server: String? - static var serverPubKey: String? - static var secretKey: [UInt8]? - static var encryptionKey: SymmetricKey? { + private static var network: String? + private static var server: String? + private static var serverPubKey: String? + private static var secretKey: [UInt8]? + private static var encryptionKey: SymmetricKey? { if let secretKey { return SymmetricKey(data: secretKey) } else { return nil } } - static var pubKey: [UInt8]? - static var cachedBearer: BackupRetrieveBearer? + private static var pubKey: [UInt8]? + private static var cachedBearer: BackupRetrieveBearer? static var requiresSetup: Bool { return server == nil @@ -180,14 +182,6 @@ class BackupClient { } } } - - static func verifySignature(message: String, signature: String, pubKey: String) -> Bool { - return Bindings.swiftVerify( - msg: [UInt8](message.data(using: .utf8)!), - sig: signature, - pk: pubKey.hexaBytes - ) - } static func persist(_ label: Label, _ bytes: [UInt8]) throws { struct PersistResponse: Codable { @@ -205,11 +199,8 @@ class BackupClient { } let pubKeyHex = Data(pubKey).hexEncodedString() - let encryptedBackup = try encrypt(Data(bytes)) - let hashToSign = hash(encryptedBackup) - - let signedHash = try sign(hashToSign) + let signedHash = try sign(hash(encryptedBackup)) //Hash of pubkey+timestamp let clientChallenge = hash("\(pubKeyHex)\(Date().timeIntervalSince1970)".data(using: .utf8)!) @@ -266,7 +257,7 @@ class BackupClient { throw BackupError.missingResponse } - guard verifySignature(message: "\(signedMessagePrefix)\(clientChallenge)", signature: persistResponse.signature, pubKey: serverPubKey) else { + guard verifySignature(message: clientChallenge, signature: persistResponse.signature, pubKey: serverPubKey) else { throw BackupError.serverChallengeResponseFailed } @@ -423,6 +414,14 @@ class BackupClient { return signed.getValue()! } + static func verifySignature(message: String, signature: String, pubKey: String) -> Bool { + return Bindings.swiftVerify( + msg: [UInt8]("\(signedMessagePrefix)\(message)".data(using: .utf8)!), + sig: signature, + pk: pubKey.hexaBytes + ) + } + private static func authToken() throws -> String { //Return cached token if still fresh if let cachedBearer { diff --git a/lib/ios/Ldk.swift b/lib/ios/Ldk.swift index c1a402cc..5a10ca5c 100644 --- a/lib/ios/Ldk.swift +++ b/lib/ios/Ldk.swift @@ -101,6 +101,7 @@ enum LdkCallbackResponses: String { case abandon_payment_success = "abandon_payment_success" case backup_client_setup_success = "backup_client_setup_success" case backup_restore_success = "backup_restore_success" + case backup_client_check_success = "backup_client_check_success" } enum LdkFileNames: String { @@ -1319,7 +1320,7 @@ class Ldk: NSObject { do { try BackupClient.selfCheck() - handleResolve(resolve, .backup_client_setup_success) + handleResolve(resolve, .backup_client_check_success) } catch { handleReject(reject, .backup_check_failed, error, error.localizedDescription) } diff --git a/lib/src/lightning-manager.ts b/lib/src/lightning-manager.ts index 317e1c1b..d60e916b 100644 --- a/lib/src/lightning-manager.ts +++ b/lib/src/lightning-manager.ts @@ -357,11 +357,11 @@ class LightningManager { } //TODO remove after dev - // const backupCheckRes = await ldk.backupSelfCheck(); - // if (backupCheckRes.isErr()) { - // console.error('Backup check failed', backupCheckRes.error); - // return err(backupCheckRes.error); - // } + const backupCheckRes = await ldk.backupSelfCheck(); + if (backupCheckRes.isErr()) { + console.error('Backup check failed', backupCheckRes.error); + return err(backupCheckRes.error); + } } // Step 1: Initialize the FeeEstimator