diff --git a/backup-server/.env.template b/backup-server/.env.template new file mode 100644 index 00000000..388c38bf --- /dev/null +++ b/backup-server/.env.template @@ -0,0 +1,5 @@ +HOST=0.0.0.0 +PORT=3003 +# npm run create-keypair +SECRET_KEY= +PUBLIC_KEY= diff --git a/backup-server/README.md b/backup-server/README.md new file mode 100644 index 00000000..dfd68626 --- /dev/null +++ b/backup-server/README.md @@ -0,0 +1,46 @@ +# react-native-ldk backup server + +This a server allows apps using the react-native-ldk to persist all node state remotely and can be restored only using the seed. + +## Running the server +``` bash +npm i + +cp .env.template .env + +npm run create-keypair + +#Paste new key pair in .env + +npm start +``` +** Remember to update wallet env with new backup server pub key + +## Clients +[Swift](https://github.com/synonymdev/react-native-ldk/blob/master/lib/ios/Classes/BackupClient.swift) +[Kotlin](https://github.com/synonymdev/react-native-ldk/blob/master/lib/android/src/main/java/com/reactnativeldk/classes/BackupClient.kt) +[NodeJS](https://github.com/synonymdev/react-native-ldk/blob/master/backup-server/src/test.js) + +## Persiting +All message signing/verifying is done using [LDK's node signing](https://docs.rs/lightning/latest/lightning/util/message_signing/fn.sign.html) on the client and [ln-verifymessagejs](https://github.com/SeverinAlexB/ln-verifymessagejs) on the server. + +1. Payload is encrypted using using standard AES/GCM encryption with the encryption key being the node secret. +2. Client creates a hash of encrypted backup and signs it. +3. Client creates unique challenge in this format: `sha256_hash(pubkey+timestamp)` +4. Client uploads encrypted bytes along with node pubkey, signed hash and challenge in request header. +5. Server hashes received payload and validates client's signed hash was actually signed by provided pubkey. +6. Server stores encrypted bytes to disk. +7. Server signs client's challenge and returns signature in response. +8. Client validate that the bytes were stored by the correct server by checking the signature in the response was signed by the server pubkey hard coded in the client. + +## Retrieving +Retieving or querying a backup requires a bearer token first done by a fairly standard challenge/response using the same node signing. + +1. Client fetches challenge from server by posting timestamp (nonce) and signed (signed timestamp) in body with pubkey in the header. +2. Server validates signature and returns challenge (32 bytes hex string). +3. Client signs challenge. +4. Client posts signed challenge with pubkey in the header. +5. Server validates signature. +6. On success server returns bearer token with 5min expiry. A long expiry isn't needed as token is only used briefly to perform a restore. +7. Client uses bearer token to pull list of backed up files. +8. Client iterates through list and downloads each file and persists to disk. \ No newline at end of file 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 43ea2dde..495a305b 100644 --- a/lib/android/src/main/java/com/reactnativeldk/classes/BackupClient.kt +++ b/lib/android/src/main/java/com/reactnativeldk/classes/BackupClient.kt @@ -152,20 +152,25 @@ class BackupClient { return hash.joinToString("") { String.format("%02x", it) } } + @Throws(BackupError::class) private fun decrypt(blob: ByteArray): ByteArray { - if (encryptionKey == null) { - throw BackupError.requiresSetup + try { + if (encryptionKey == null) { + throw BackupError.requiresSetup + } + + val nonce = blob.take(12).toByteArray() + val encrypted = blob.copyOfRange(12, blob.size) + + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + val gcmSpec = GCMParameterSpec(128, nonce) + + cipher.init(Cipher.DECRYPT_MODE, encryptionKey, gcmSpec) + val decryptedBytes = cipher.doFinal(encrypted) + return decryptedBytes + } catch (e: Exception) { + throw DecryptFailed(e.message ?: "") } - - val nonce = blob.take(12).toByteArray() - val encrypted = blob.copyOfRange(12, blob.size) - - val cipher = Cipher.getInstance("AES/GCM/NoPadding") - val gcmSpec = GCMParameterSpec(128, nonce) - - cipher.init(Cipher.DECRYPT_MODE, encryptionKey, gcmSpec) - val decryptedBytes = cipher.doFinal(encrypted) - return decryptedBytes } @Throws(BackupError::class) diff --git a/lib/src/lightning-manager.ts b/lib/src/lightning-manager.ts index d60e916b..1035aceb 100644 --- a/lib/src/lightning-manager.ts +++ b/lib/src/lightning-manager.ts @@ -355,13 +355,6 @@ class LightningManager { if (backupSetupRes.isErr()) { return err(backupSetupRes.error); } - - //TODO remove after dev - const backupCheckRes = await ldk.backupSelfCheck(); - if (backupCheckRes.isErr()) { - console.error('Backup check failed', backupCheckRes.error); - return err(backupCheckRes.error); - } } // Step 1: Initialize the FeeEstimator