Skip to content

Commit

Permalink
feat: android remote backup restore
Browse files Browse the repository at this point in the history
  • Loading branch information
Jasonvdb committed Sep 14, 2023
1 parent 71dd44f commit 98df4c4
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 100 deletions.
2 changes: 1 addition & 1 deletion backup-server/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ fastify.route({

const {network} = query;
const bearerToken = headers.authorization;
const pubkey = fancyUserDB.get(bearerToken);
const pubkey = users.get(bearerToken);

const list = storage.list({pubkey, network});
const channelMonitorList = storage.list({pubkey, network, subdir: 'channel_monitors'});
Expand Down
2 changes: 1 addition & 1 deletion example/ldk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export const getBackupServerDetails =
const backupServerDetails: TBackupServerDetails = {
url: backupServer,
token:
'de2699b89e6c3c93ac9402aea4f886da478f8c5fd017d326bee1188aaf6b236cc85ee92c0138490b1b2cb434b04a116de12e2965dc9dc212a4c1949550a5a9f3',
'c14813666b79958e2ba830172bf3f4bca78c4782e5c8d3933237728cec2df8643fe0f6d540e6e92030148dcda578e70f8e93338f1f004ac38ea2b673d7f4224c',
};

return backupServerDetails;
Expand Down
50 changes: 47 additions & 3 deletions lib/android/src/main/java/com/reactnativeldk/LdkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -227,19 +227,61 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
BackupClient.skipRemoteBackup = false
BackupClient.setup(seed.hexa(), network, server, token)

val ping = "ping ${Random().nextInt(1000)}"
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) {
Expand Down Expand Up @@ -1027,8 +1069,6 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
fun writeToFile(fileName: String, path: String, content: String, format: String, remotePersist: Boolean, promise: Promise) {
val file: File

//TODO HANDLE REMOTE PERSIST

try {
if (path != "") {
//Make sure custom path exists by creating if missing
Expand All @@ -1053,6 +1093,10 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
file.writeText(content)
}

if (remotePersist) {
BackupClient.persist(BackupClient.Label.MISC(fileName), file.readBytes())
}

handleResolve(promise, LdkCallbackResponses.file_write_success)
} catch (e: Exception) {
handleReject(promise, LdkErrors.write_fail, Error(e))
Expand Down
122 changes: 57 additions & 65 deletions lib/android/src/main/java/com/reactnativeldk/classes/BackupClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import com.reactnativeldk.EventTypes
import com.reactnativeldk.LdkEventEmitter
import com.reactnativeldk.hexEncodedString
import com.reactnativeldk.hexa
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL
import java.security.SecureRandom
Expand All @@ -26,6 +27,11 @@ 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 CompleteBackup(
val files: Map<String, ByteArray>,
val channelFiles: Map<String, ByteArray>
)

class BackupClient {
sealed class Label(val string: String, channelId: String = "") {
data class PING(val customName: String = "") : Label("ping")
Expand All @@ -52,6 +58,10 @@ class BackupClient {
var encryptionKey: SecretKeySpec? = null
var token: String? = null

var requiresSetup = {
server == null
}

fun setup(seed: ByteArray, network: String, server: String, token: String) {
this.network = network
this.server = server
Expand Down Expand Up @@ -104,48 +114,6 @@ class BackupClient {
return iv + cipherText
}

@Throws(BackupError::class)
private fun encrypt_old(blob: ByteArray): ByteArray {
if (encryptionKey == null) {
throw BackupError.requiresSetup
}

synchronized(Cipher::class.java) {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")

//TODO generate 12 bytes nonce randomly
val nonce = "131348c0987c7eece60fc0bc".hexa()

cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, IvParameterSpec(nonce))

val cipherText = ByteArray(cipher.getOutputSize(blob.size))
var ctLength = cipher.update(blob, 0, blob.size, cipherText, 0)
ctLength += cipher.doFinal(cipherText, ctLength)

LdkEventEmitter.send(
EventTypes.native_log,
"BEFORE ENCRYPTED: ${blob.hexEncodedString()}"
)
LdkEventEmitter.send(
EventTypes.native_log,
"SAVED ENCRYPTED: ${cipherText.hexEncodedString()}"
)
LdkEventEmitter.send(
EventTypes.native_log,
"NONCE: ${nonce.hexEncodedString()}"
)

val combined = nonce.plus(cipherText)

LdkEventEmitter.send(
EventTypes.native_log,
"COMBINED: ${combined.hexEncodedString()}"
)

return combined
}
}

private fun decrypt(blob: ByteArray): ByteArray {
if (encryptionKey == null) {
throw BackupError.requiresSetup
Expand Down Expand Up @@ -189,12 +157,8 @@ class BackupClient {
val responseCode = urlConnection.responseCode
LdkEventEmitter.send(
EventTypes.native_log,
"Sent 'POST' request to URL: $url; Response Code: $responseCode"
"Remote persist success for ${label.string}"
)

//TODO for retrieve
val inputStream = urlConnection.inputStream
inputStream.close()
}

@Throws(BackupError::class)
Expand All @@ -218,6 +182,11 @@ class BackupClient {
}

if (responseCode != 200) {
LdkEventEmitter.send(
EventTypes.native_log,
"Remote retrieve failed for ${label.string} with response code $responseCode"
)

throw InvalidServerResponse(responseCode)
}

Expand All @@ -227,37 +196,60 @@ class BackupClient {

LdkEventEmitter.send(
EventTypes.native_log,
"Received encrypted backup: ${encryptedBackup.hexEncodedString()}"
"Remote retrieve success for ${label.string}"
)

val decryptedBackup = decrypt(encryptedBackup)

return decryptedBackup
}

fun testUrls() {
LdkEventEmitter.send(EventTypes.native_log, "Hello from BackupClient")
@Throws(BackupError::class)
fun retrieveCompleteBackup(): CompleteBackup {
val url = backupUrl(Method.LIST)

LdkEventEmitter.send(
EventTypes.native_log,
"backupUrl: ${backupUrl(Method.PERSIST, Label.PING())}"
)
LdkEventEmitter.send(
EventTypes.native_log,
"backupUrl: ${backupUrl(Method.PERSIST, Label.CHANNEL_MANAGER())}"
"Retrieving backup from $url"
)

val channelMonitorLabel = Label.CHANNEL_MONITOR("", "abcd1234")
LdkEventEmitter.send(
EventTypes.native_log,
"backupUrl: ${backupUrl(Method.PERSIST, channelMonitorLabel, "1234")}"
)
val urlConnection = url.openConnection() as HttpURLConnection
urlConnection.requestMethod = "GET"
urlConnection.setRequestProperty("Content-Type", "application/json")
urlConnection.setRequestProperty("Authorization", token)

val misc = Label.MISC("random.bin")
LdkEventEmitter.send(
EventTypes.native_log,
"backupUrl: ${backupUrl(Method.RETRIEVE, misc)}"
)
val responseCode = urlConnection.responseCode

if (responseCode == 404) {
throw BackupError.missingBackup
}

if (responseCode != 200) {
throw InvalidServerResponse(responseCode)
}

val inputStream = urlConnection.inputStream
val jsonString = inputStream.bufferedReader().use { it.readText() }
inputStream.close()

val jsonObject = JSONObject(jsonString)

val files = mutableMapOf<String, ByteArray>()
val fileNames = jsonObject.getJSONArray("list")
for (i in 0 until fileNames.length()) {
val filename = fileNames.getString(i)
files[filename] = retrieve(Label.MISC(filename))
}

val channelFiles = mutableMapOf<String, ByteArray>()
val channelFileNames = jsonObject.getJSONArray("channel_monitors")
for (i in 0 until channelFileNames.length()) {
val filename = channelFileNames.getString(i)

channelFiles[filename] = retrieve(Label.CHANNEL_MONITOR(channelId=filename.replace(".bin", "")))
}

return CompleteBackup(files = files, channelFiles = channelFiles)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ class LdkChannelManagerPersister: ChannelManagerConstructor.EventHandler {
override fun persist_manager(channel_manager_bytes: ByteArray?) {
if (channel_manager_bytes != null && LdkModule.accountStoragePath != "") {
File(LdkModule.accountStoragePath + "/" + LdkFileNames.channel_manager.fileName).writeBytes(channel_manager_bytes)

BackupClient.persist(BackupClient.Label.CHANNEL_MANAGER(), channel_manager_bytes)

LdkEventEmitter.send(EventTypes.native_log, "Persisted channel manager to disk")
LdkEventEmitter.send(EventTypes.backup, "")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ class LdkPersister {
throw Exception("Channel storage path not set")
}

val file = File(LdkModule.channelStoragePath + "/" + id.to_channel_id().hexEncodedString() + ".bin")
val channelId = id.to_channel_id().hexEncodedString()

val file = File(LdkModule.channelStoragePath + "/" + channelId + ".bin")

val isNew = !file.exists()

file.writeBytes(data.write())
BackupClient.persist(BackupClient.Label.CHANNEL_MONITOR(channelId=channelId), data.write())

LdkEventEmitter.send(EventTypes.native_log, "Persisted channel (${id.to_channel_id().hexEncodedString()}) to disk")
LdkEventEmitter.send(EventTypes.backup, "")
Expand Down
25 changes: 2 additions & 23 deletions lib/ios/Classes/BackupClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,20 +122,14 @@ class BackupClient {

let sealedBox = try AES.GCM.seal(blob, using: key)

// let sealedBox = try AES.GCM.seal(blob, using: key, nonce: try! AES.GCM.Nonce(data: nonce))
print("NONC E BYTES: \(Data(sealedBox.nonce).count)")
// let payload = Data(sealedBox.nonce) + sealedBox.combined!
// sealedBox.
return sealedBox.combined!
}

private static func decrypt(_ blob: Data) throws -> Data {
guard let key = Self.encryptionKey else {
throw BackupError.requiresSetup
}

// let extractedNonce = try! AES.GCM.Nonce(data: nonce)


//Remove appended 12 bytes nonce and 16 byte trailing tag
let encryptedData: Data = {
var bytes = blob.subdata(in: 12..<blob.count)
Expand All @@ -146,23 +140,10 @@ class BackupClient {
let nonce = blob.prefix(12)
let tag = blob.suffix(16)

print("ENCRYPTED: \(encryptedData.hexEncodedString())")
print("NONCE: \(nonce.hexEncodedString())")

//KOTL
// BEFORE ENCRYPTED: 70696e672d66726f6d2d696f73
// LOG react-native-ldk: Received encrypted backup: 131348c0987c7eece60fc0bc5c34e4a777541996c36dee95cd34e07349ea8de2
// LOG react-native-ldk: FETCHED ENCRYPTED: 131348c0987c7eece60fc0bc5c34e4a777541996c36dee95cd34e07349ea8de2
// LOG react-native-ldk: NONCE: 131348c0987c7eece60fc0bc


do {
// let sealedBox = try AES.GCM.SealedBox(combined: blob)
let sealedBox = try AES.GCM.SealedBox(nonce: .init(data: nonce), ciphertext: encryptedData, tag: tag)
let decryptedData = try AES.GCM.open(sealedBox, using: key)

print("DECRYPTPED NONCE: \(Data(sealedBox.nonce).hexEncodedString())")


return decryptedData
} catch {
if let ce = error as? CryptoKitError {
Expand All @@ -174,7 +155,6 @@ class BackupClient {
}

//TODO authentication
//TODO restore

static func persist(_ label: Label, _ bytes: [UInt8]) throws {
guard !skipRemoteBackup else {
Expand Down Expand Up @@ -341,6 +321,5 @@ class BackupClient {
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Remote list files success.")

return CompleteBackup(files: allFiles, channelFiles: channelFiles)

}
}
5 changes: 2 additions & 3 deletions lib/ios/Classes/LdkPersist.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ class LdkPersister: Persist {
}

let isNew = !FileManager().fileExists(atPath: channelStoragePath.path)


try Data(data.write()).write(to: channelStoragePath)
try BackupClient.persist(.channelMonitor(id: channelId), data.write())

try Data(data.write()).write(to: channelStoragePath)
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Persisted channel (\(channelId)) to disk")
LdkEventEmitter.shared.send(withEvent: .backup, body: "")

try BackupClient.persist(.channelMonitor(id: channelId), data.write())

if isNew {
LdkEventEmitter.shared.send(
Expand Down
Loading

0 comments on commit 98df4c4

Please sign in to comment.