Skip to content

Commit

Permalink
feat: android bearer token auth for restoring backups
Browse files Browse the repository at this point in the history
  • Loading branch information
Jasonvdb committed Sep 29, 2023
1 parent 176497f commit d3136e9
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 14 deletions.
1 change: 0 additions & 1 deletion backup-server/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ const authRetrieveCheckHandler = async (request, reply) => {

if (!bearerToken || !users.has(bearerToken)) {
fastify.log.error("Unauthorized or missing token");
console.warn(`\n\nbearerToken: ${bearerToken}\n\n`)
reply.code(401);
return reply.send({error: "Unauthorized"});
}
Expand Down
11 changes: 10 additions & 1 deletion example/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { TAvailableNetworks } from '@synonymdev/react-native-ldk';
import {
TAvailableNetworks,
TBackupServerDetails,
} from '@synonymdev/react-native-ldk';

export const selectedNetwork: TAvailableNetworks = 'bitcoinRegtest';

Expand Down Expand Up @@ -44,3 +47,9 @@ export const customPeers = {
],
bitcoinSignet: [],
};

export const backupServerDetails: TBackupServerDetails = {
host: 'https://jaybird-logical-sadly.ngrok-free.app',
serverPubKey:
'0343a6c1b7700840ac7b76372617a6e9a05cf4c9716efdc847def65360b238f243',
};
105 changes: 97 additions & 8 deletions lib/android/src/main/java/com/reactnativeldk/classes/BackupClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class BackupError : Exception() {
val decryptFailed = DecryptFailed("")
val signingError = SigningError()
val serverChallengeResponseFailed = ServerChallengeResponseFailed()
val checkError = BackupCheckError()
}
}

Expand All @@ -36,6 +37,7 @@ class InvalidServerResponse(code: Int) : Exception("Invalid backup server respon
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 BackupCheckError() : Exception("Backup self check failed")

class CompleteBackup(
val files: Map<String, ByteArray>,
Expand All @@ -62,6 +64,11 @@ class BackupClient {
AUTH_RESPONSE("auth/response")
}

class CachedBearer(
val bearer: String,
val expires: Long
)

private var version = "v1"
private var signedMessagePrefix = "react-native-ldk backup server auth:"

Expand All @@ -78,6 +85,7 @@ class BackupClient {
null
}
private var pubKey: ByteArray? = null
private var cachedBearer: CachedBearer? = null

val requiresSetup: Boolean
get() = server == null
Expand Down Expand Up @@ -223,7 +231,7 @@ class BackupClient {

@Throws(BackupError::class)
fun retrieve(label: Label): ByteArray {
val bearer = "TODO"
val bearer = authToken()
val url = backupUrl(Method.RETRIEVE, label)

LdkEventEmitter.send(
Expand Down Expand Up @@ -267,7 +275,7 @@ class BackupClient {

@Throws(BackupError::class)
fun retrieveCompleteBackup(): CompleteBackup {
val bearer = "TODO"
val bearer = authToken()

val url = backupUrl(Method.LIST)

Expand Down Expand Up @@ -320,14 +328,18 @@ class BackupClient {
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) {
//
// }
val pingRetrieved = retrieve(Label.PING())
if (pingRetrieved.toString(Charsets.UTF_8) != ping) {
LdkEventEmitter.send(
EventTypes.native_log,
"Backup check failed to verify ping content."
)

throw BackupError.checkError
}
}

fun sign(message: String): String {
private fun sign(message: String): String {
if (secretKey == null) {
throw BackupError.requiresSetup
}
Expand All @@ -347,5 +359,82 @@ class BackupClient {
pubKey.hexa()
)
}

@Throws(BackupError::class)
private fun authToken(): String {
if (cachedBearer != null && cachedBearer!!.expires > System.currentTimeMillis()) {
return cachedBearer!!.bearer
}

if (pubKey == null) {
throw BackupError.requiresSetup
}

//Fetch challenge with signed timestamp as nonce
val pubKeyHex = pubKey!!.hexEncodedString()
val timestamp = System.currentTimeMillis()
val payload = JSONObject(
mapOf(
"timestamp" to timestamp,
"signature" to sign(timestamp.toString())
)
)

val url = backupUrl(Method.AUTH_CHALLENGE)

val urlConnection = url.openConnection() as HttpURLConnection
urlConnection.requestMethod = "POST"
urlConnection.setRequestProperty("Content-Type", "application/json")
urlConnection.setRequestProperty("Public-Key", pubKeyHex)
val outputStream = urlConnection.outputStream
outputStream.write(payload.toString().toByteArray())
outputStream.close()

if (urlConnection.responseCode != 200) {
LdkEventEmitter.send(
EventTypes.native_log,
"Fetch server challenge failed."
)

throw InvalidServerResponse(urlConnection.responseCode)
}

val inputStream = urlConnection.inputStream
val jsonString = inputStream.bufferedReader().use { it.readText() }
inputStream.close()
val challenge = JSONObject(jsonString).getString("challenge")

//Sign challenge and fetch bearer token
val urlBearer = backupUrl(Method.AUTH_RESPONSE)
val urlConnectionBearer = urlBearer.openConnection() as HttpURLConnection
urlConnectionBearer.requestMethod = "POST"
urlConnectionBearer.setRequestProperty("Content-Type", "application/json")
urlConnectionBearer.setRequestProperty("Public-Key", pubKeyHex)
val outputStreamBearer = urlConnectionBearer.outputStream
outputStreamBearer.write(JSONObject(
mapOf(
"signature" to sign(challenge)
)
).toString().toByteArray())
outputStreamBearer.close()

if (urlConnectionBearer.responseCode != 200) {
LdkEventEmitter.send(
EventTypes.native_log,
"Fetch bearer token failed."
)

throw InvalidServerResponse(urlConnection.responseCode)
}

val inputStreamBearer = urlConnectionBearer.inputStream
val jsonBearer = JSONObject(inputStreamBearer.bufferedReader().use { it.readText() })
inputStreamBearer.close()
val bearer = jsonBearer.getString("bearer")
val expires = jsonBearer.getLong("expires")

cachedBearer = CachedBearer(bearer, expires)
return bearer
}
}
}
5 changes: 1 addition & 4 deletions lib/ios/Classes/BackupClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,6 @@ struct BackupRetrieveBearer: Codable {
}

class BackupClient {


enum Label {
case ping
case channelManager
Expand Down Expand Up @@ -430,12 +428,11 @@ class BackupClient {
}
}

//Fetch challenge
guard let pubKey else {
throw BackupError.requiresSetup
}

//Signed timestamp as nonce
//Fetch challenge with signed timestamp as nonce
let pubKeyHex = Data(pubKey).hexEncodedString()
let timestamp = String(Date().timeIntervalSince1970)

Expand Down

0 comments on commit d3136e9

Please sign in to comment.