Add v4 onion request handling

This commit is contained in:
ceokot 2022-03-28 08:08:08 +02:00
parent 85456b5ea2
commit b51013f050
15 changed files with 388 additions and 184 deletions

View File

@ -8,6 +8,7 @@ import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsignal.utilities.toHexString
@RunWith(AndroidJUnit4::class)
class SodiumUtilitiesTest {
@ -25,4 +26,27 @@ class SodiumUtilitiesTest {
assertThat(keyPair.publicKey.asHexString.lowercase(), equalTo(blindedKey))
}
@Test
fun sharedBlindedEncryptionKey() {
val key = ByteArray(0)
val encryptionKey = SodiumUtilities.sharedBlindedEncryptionKey(key, key, key, key)
}
@Test
fun sogsSignature() {
// val expectedSignature = "K1N3A+H4dxV/wiN6Mr9cEj9TWUUqxESDoGW1cmoqDp7zMzCuCraTQKPX1tIiPuOBmFvB8VSUuYsHZrfGis1hDA=="
// val expectedSignature = "xxLpXHbomAJMB9AtGMyqvBsXrdd2040y+Ol/IKzElWfKJa3EYZRv1GLO6CTLhrDFUwVQe8PPltyGs54Kd7O5Cg=="
val expectedSignature = "gYqpWZX6fnF4Gb2xQM3xaXs0WIYEI49+B8q4mUUEg8Rw0ObaHUWfoWjMHMArAtP9QlORfiydsKWz1o6zdPVeCQ=="
val keyPair = SodiumUtilities.blindedKeyPair(serverPublicKey, KeyPair(pubKey, secKey))!!
val signature = SodiumUtilities.sogsSignature(
ByteArray(0),
secKey.asBytes,
keyPair.secretKey.asBytes,
keyPair.publicKey.asBytes
)!!
assertThat(signature.toHexString(), equalTo(expectedSignature))
}
}

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import org.session.libsession.messaging.file_server.FileServerAPIV2;
import org.session.libsession.messaging.file_server.FileServerApi;
public class PushMediaConstraints extends MediaConstraints {
@ -21,26 +21,26 @@ public class PushMediaConstraints extends MediaConstraints {
@Override
public int getImageMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
}
@Override
public int getGifMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
}
@Override
public int getVideoMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
}
@Override
public int getAudioMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
}
@Override
public int getDocumentMaxSize(Context context) {
return (int) (((double) FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier);
return (int) (((double) FileServerApi.maxFileSize) / FileServerApi.fileSizeORMultiplier);
}
}

View File

@ -43,7 +43,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json ->
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, false)
@ -72,7 +72,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json ->
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, true)
@ -100,7 +100,7 @@ object LokiPushNotificationManager {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, "/loki/v2/lsrpc").map { json ->
OnionRequestAPI.sendOnionRequest(request.build(), server, pnServerPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
if (code == null || code == 0) {
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")

View File

@ -13,7 +13,7 @@ import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
object FileServerAPIV2 {
object FileServerApi {
private const val serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
const val server = "http://filev2.getsession.org"
@ -73,12 +73,12 @@ object FileServerAPIV2 {
HTTP.Verb.POST -> requestBuilder.post(createBody(request.parameters)!!)
HTTP.Verb.DELETE -> requestBuilder.delete(createBody(request.parameters))
}
if (request.useOnionRouting) {
return OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).fail { e ->
return if (request.useOnionRouting) {
OnionRequestAPI.sendOnionRequest(requestBuilder.build(), server, serverPublicKey).fail { e ->
Log.e("Loki", "File server request failed.", e)
}
} else {
return Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
Promise.ofFail(IllegalStateException("It's currently not allowed to send non onion routed requests."))
}
}

View File

@ -6,9 +6,8 @@ import com.esotericsoftware.kryo.io.Output
import nl.komponents.kovenant.Promise
import okio.Buffer
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsession.messaging.messages.Message
import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
import org.session.libsession.messaging.open_groups.OpenGroupApiV4
import org.session.libsession.messaging.sending_receiving.MessageSender
import org.session.libsession.messaging.utilities.Data
@ -58,8 +57,8 @@ class AttachmentUploadJob(val attachmentID: Long, val threadID: String, val mess
}
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
} else {
val keyAndResult = upload(attachment, FileServerAPIV2.server, true) {
FileServerAPIV2.upload(it)
val keyAndResult = upload(attachment, FileServerApi.server, true) {
FileServerApi.upload(it)
}
handleSuccess(attachment, keyAndResult.first, keyAndResult.second)
}

View File

@ -38,7 +38,7 @@ class NotifyPNServerJob(val message: SnodeMessage) : Job {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(4) {
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, "/loki/v2/lsrpc").map { json ->
OnionRequestAPI.sendOnionRequest(request.build(), server, PushNotificationAPI.serverPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
if (code == null || code == 0) {
Log.d("Loki", "Couldn't notify PN server due to error: ${json["message"] as? String ?: "null"}.")

View File

@ -20,7 +20,6 @@ import org.session.libsession.messaging.utilities.SodiumUtilities
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.utilities.AESGCM
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Base64.decode
import org.session.libsignal.utilities.Base64.encodeBytes
import org.session.libsignal.utilities.HTTP

View File

@ -38,7 +38,7 @@ object PushNotificationAPI {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json ->
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, false)
@ -66,7 +66,7 @@ object PushNotificationAPI {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json ->
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
if (code != null && code != 0) {
TextSecurePreferences.setIsUsingFCM(context, true)
@ -93,7 +93,7 @@ object PushNotificationAPI {
val body = RequestBody.create(MediaType.get("application/json"), JsonUtil.toJson(parameters))
val request = Request.Builder().url(url).post(body)
retryIfNeeded(maxRetryCount) {
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, "/loki/v2/lsrpc").map { json ->
OnionRequestAPI.sendOnionRequest(request.build(), server, serverPublicKey, OnionRequestAPI.Version.V2).map { json ->
val code = json["code"] as? Int
if (code == null || code == 0) {
Log.d("Loki", "Couldn't subscribe/unsubscribe closed group: $closedGroupPublicKey due to error: ${json["message"] as? String ?: "null"}.")

View File

@ -47,8 +47,8 @@ object SodiumUtilities {
/* Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` */
fun blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair): KeyPair? {
// if (edKeyPair.publicKey.asBytes.size != Sign.PUBLICKEYBYTES ||
// edKeyPair.secretKey.asBytes.size != Sign.SECRETKEYBYTES) return null
if (edKeyPair.publicKey.asBytes.size != Sign.PUBLICKEYBYTES ||
edKeyPair.secretKey.asBytes.size != Sign.SECRETKEYBYTES) return null
val kBytes = generateBlindingFactor(serverPublicKey)
val aBytes = generatePrivateKeyScalar(edKeyPair.secretKey.asBytes)
// Generate the blinded key pair `ka`, `kA`
@ -183,17 +183,17 @@ object SodiumUtilities {
}
val hexString
get() = prefix?.prefix + publicKey
get() = prefix?.value + publicKey
}
enum class IdPrefix(val prefix: String) {
enum class IdPrefix(val value: String) {
STANDARD("05"), BLINDED("15"), UN_BLINDED("00");
companion object {
fun fromValue(prefix: String): IdPrefix? = when(prefix) {
STANDARD.prefix -> STANDARD
BLINDED.prefix -> BLINDED
UN_BLINDED.prefix -> UN_BLINDED
fun fromValue(rawValue: String): IdPrefix? = when(rawValue) {
STANDARD.value -> STANDARD
BLINDED.value -> BLINDED
UN_BLINDED.value -> UN_BLINDED
else -> null
}
}

View File

@ -1,27 +1,58 @@
package org.session.libsession.snode
import nl.komponents.kovenant.Deferred
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.all
import nl.komponents.kovenant.deferred
import nl.komponents.kovenant.functional.bind
import nl.komponents.kovenant.functional.map
import okhttp3.Request
import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsession.utilities.AESGCM
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.*
import org.session.libsignal.utilities.Snode
import org.session.libsession.utilities.AESGCM.EncryptionResult
import org.session.libsession.utilities.getBodyForOnionRequest
import org.session.libsession.utilities.getHeadersForOnionRequest
import org.session.libsignal.crypto.getRandomElement
import org.session.libsignal.crypto.getRandomElementOrNull
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Broadcaster
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import java.util.*
import kotlin.math.abs
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.recover
import org.session.libsignal.utilities.toHexString
import java.util.Date
import kotlin.collections.List
import kotlin.collections.Map
import kotlin.collections.Set
import kotlin.collections.any
import kotlin.collections.contains
import kotlin.collections.count
import kotlin.collections.dropLast
import kotlin.collections.filter
import kotlin.collections.first
import kotlin.collections.firstOrNull
import kotlin.collections.flatten
import kotlin.collections.forEach
import kotlin.collections.get
import kotlin.collections.indexOfFirst
import kotlin.collections.isNotEmpty
import kotlin.collections.last
import kotlin.collections.listOf
import kotlin.collections.map
import kotlin.collections.mapOf
import kotlin.collections.minus
import kotlin.collections.mutableMapOf
import kotlin.collections.mutableSetOf
import kotlin.collections.plus
import kotlin.collections.set
import kotlin.collections.setOf
import kotlin.collections.toMutableList
import kotlin.collections.toSet
import kotlin.collections.toString
private typealias Path = List<Snode>
@ -96,7 +127,8 @@ object OnionRequestAPI {
ThreadUtils.queue { // No need to block the shared context for this
val url = "${snode.address}:${snode.port}/get_stats/v1"
try {
val json = HTTP.execute(HTTP.Verb.GET, url, 3)
val response = HTTP.execute(HTTP.Verb.GET, url, 3).decodeToString()
val json = JsonUtil.fromJson(response, Map::class.java)
val version = json["version"] as? String
if (version == null) { deferred.reject(Exception("Missing snode version.")); return@queue }
if (version >= "2.0.7") {
@ -209,26 +241,30 @@ object OnionRequestAPI {
}
OnionRequestAPI.guardSnodes = guardSnodes
fun getPath(paths: List<Path>): Path {
if (snodeToExclude != null) {
return paths.filter { !it.contains(snodeToExclude) }.getRandomElement()
return if (snodeToExclude != null) {
paths.filter { !it.contains(snodeToExclude) }.getRandomElement()
} else {
return paths.getRandomElement()
paths.getRandomElement()
}
}
if (paths.count() >= targetPathCount) {
return Promise.of(getPath(paths))
} else if (paths.isNotEmpty()) {
if (paths.any { !it.contains(snodeToExclude) }) {
buildPaths(paths) // Re-build paths in the background
when {
paths.count() >= targetPathCount -> {
return Promise.of(getPath(paths))
} else {
return buildPaths(paths).map { newPaths ->
getPath(newPaths)
}
paths.isNotEmpty() -> {
return if (paths.any { !it.contains(snodeToExclude) }) {
buildPaths(paths) // Re-build paths in the background
Promise.of(getPath(paths))
} else {
buildPaths(paths).map { newPaths ->
getPath(newPaths)
}
}
}
} else {
return buildPaths(listOf()).map { newPaths ->
getPath(newPaths)
else -> {
return buildPaths(listOf()).map { newPaths ->
getPath(newPaths)
}
}
}
}
@ -270,7 +306,11 @@ object OnionRequestAPI {
/**
* Builds an onion around `payload` and returns the result.
*/
private fun buildOnionForDestination(payload: Map<*, *>, destination: Destination): Promise<OnionBuildingResult, Exception> {
private fun buildOnionForDestination(
payload: ByteArray,
destination: Destination,
version: Version
): Promise<OnionBuildingResult, Exception> {
lateinit var guardSnode: Snode
lateinit var destinationSymmetricKey: ByteArray // Needed by LokiAPI to decrypt the response sent back by the destination
lateinit var encryptionResult: EncryptionResult
@ -281,19 +321,19 @@ object OnionRequestAPI {
return getPath(snodeToExclude).bind { path ->
guardSnode = path.first()
// Encrypt in reverse order, i.e. the destination first
OnionRequestEncryption.encryptPayloadForDestination(payload, destination).bind { r ->
OnionRequestEncryption.encryptPayloadForDestination(payload, destination, version).bind { r ->
destinationSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r
@Suppress("NAME_SHADOWING") var path = path
var rhs = destination
fun addLayer(): Promise<EncryptionResult, Exception> {
if (path.isEmpty()) {
return Promise.of(encryptionResult)
return if (path.isEmpty()) {
Promise.of(encryptionResult)
} else {
val lhs = Destination.Snode(path.last())
path = path.dropLast(1)
return OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind { r ->
OnionRequestEncryption.encryptHop(lhs, rhs, encryptionResult).bind { r ->
encryptionResult = r
rhs = lhs
addLayer()
@ -308,15 +348,15 @@ object OnionRequestAPI {
/**
* Sends an onion request to `destination`. Builds new paths as needed.
*/
private fun sendOnionRequest(destination: Destination, payload: Map<*, *>): Promise<Map<*, *>, Exception> {
private fun sendOnionRequest(destination: Destination, payload: ByteArray, version: Version): Promise<Map<*, *>, Exception> {
val deferred = deferred<Map<*, *>, Exception>()
lateinit var guardSnode: Snode
buildOnionForDestination(payload, destination).success { result ->
buildOnionForDestination(payload, destination, version).success { result ->
guardSnode = result.guardSnode
val url = "${guardSnode.address}:${guardSnode.port}/onion_req/v2"
val finalEncryptionResult = result.finalEncryptionResult
val onion = finalEncryptionResult.ciphertext
if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerAPIV2.maxFileSize.toDouble()) {
if (destination is Destination.Server && onion.count().toDouble() > 0.75 * FileServerApi.maxFileSize.toDouble()) {
Log.d("Loki", "Approaching request size limit: ~${onion.count()} bytes.")
}
@Suppress("NAME_SHADOWING") val parameters = mapOf(
@ -331,49 +371,8 @@ object OnionRequestAPI {
val destinationSymmetricKey = result.destinationSymmetricKey
ThreadUtils.queue {
try {
val json = HTTP.execute(HTTP.Verb.POST, url, body)
val base64EncodedIVAndCiphertext = json["result"] as? String ?: return@queue deferred.reject(Exception("Invalid JSON"))
val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext)
try {
val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey)
try {
@Suppress("NAME_SHADOWING") val json = JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java)
val statusCode = json["status_code"] as? Int ?: json["status"] as Int
if (statusCode == 406) {
@Suppress("NAME_SHADOWING") val body = mapOf( "result" to "Your clock is out of sync with the service node network." )
val exception = HTTPRequestFailedAtDestinationException(statusCode, body, destination.description)
return@queue deferred.reject(exception)
} else if (json["body"] != null) {
@Suppress("NAME_SHADOWING") val body: Map<*, *>
if (json["body"] is Map<*, *>) {
body = json["body"] as Map<*, *>
} else {
val bodyAsString = json["body"] as String
body = JsonUtil.fromJson(bodyAsString, Map::class.java)
}
if (body["t"] != null) {
val timestamp = body["t"] as Long
val offset = timestamp - Date().time
SnodeAPI.clockOffset = offset
}
if (statusCode != 200) {
val exception = HTTPRequestFailedAtDestinationException(statusCode, body, destination.description)
return@queue deferred.reject(exception)
}
deferred.resolve(body)
} else {
if (statusCode != 200) {
val exception = HTTPRequestFailedAtDestinationException(statusCode, json, destination.description)
return@queue deferred.reject(exception)
}
deferred.resolve(json)
}
} catch (exception: Exception) {
deferred.reject(Exception("Invalid JSON: ${plaintext.toString(Charsets.UTF_8)}."))
}
} catch (exception: Exception) {
deferred.reject(exception)
}
val response = HTTP.execute(HTTP.Verb.POST, url, body)
handleResponse(response, destinationSymmetricKey, destination, version, deferred)
} catch (exception: Exception) {
deferred.reject(exception)
}
@ -440,12 +439,13 @@ object OnionRequestAPI {
/**
* Sends an onion request to `snode`. Builds new paths as needed.
*/
internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, publicKey: String? = null): Promise<Map<*, *>, Exception> {
internal fun sendOnionRequest(method: Snode.Method, parameters: Map<*, *>, snode: Snode, version: Version, publicKey: String? = null): Promise<Map<*, *>, Exception> {
val payload = mapOf(
"method" to method.rawValue,
"params" to parameters
)
return sendOnionRequest(Destination.Snode(snode), payload).recover { exception ->
val payloadData = JsonUtil.toJson(payload).toByteArray()
return sendOnionRequest(Destination.Snode(snode), payloadData, version).recover { exception ->
val error = when (exception) {
is HTTP.HTTPRequestFailedException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
is HTTPRequestFailedAtDestinationException -> SnodeAPI.handleSnodeError(exception.statusCode, exception.json, snode, publicKey)
@ -461,27 +461,198 @@ object OnionRequestAPI {
*
* `publicKey` is the hex encoded public key of the user the call is associated with. This is needed for swarm cache maintenance.
*/
fun sendOnionRequest(request: Request, server: String, x25519PublicKey: String, target: String = "/loki/v3/lsrpc"): Promise<Map<*, *>, Exception> {
val headers = request.getHeadersForOnionRequest()
fun sendOnionRequest(request: Request, server: String, x25519PublicKey: String, version: Version = Version.V4): Promise<Map<*, *>, Exception> {
val url = request.url()
val payload = generatePayload(request, server, version)
val destination = Destination.Server(url.host(), version.value, x25519PublicKey, url.scheme(), url.port())
return sendOnionRequest(destination, payload, version).recover { exception ->
Log.d("Loki", "Couldn't reach server: $url due to error: $exception.")
throw exception
}
}
private fun generatePayload(request: Request, server: String, version: Version): ByteArray {
val headers = request.getHeadersForOnionRequest().toMutableMap()
val url = request.url()
val urlAsString = url.toString()
val host = url.host()
val body = request.getBodyForOnionRequest() ?: "null"
val endpoint = when {
server.count() < urlAsString.count() -> urlAsString.substringAfter(server).removePrefix("/")
else -> ""
}
val body = request.getBodyForOnionRequest() ?: "null"
val payload = mapOf(
"body" to body,
"endpoint" to endpoint,
"method" to request.method(),
"headers" to headers
)
val destination = Destination.Server(host, target, x25519PublicKey, url.scheme(), url.port())
return sendOnionRequest(destination, payload).recover { exception ->
Log.d("Loki", "Couldn't reach server: $urlAsString due to error: $exception.")
throw exception
return if (version == Version.V4) {
if (request.body() != null && !headers.containsKey("Content-Type")) {
headers["Content-Type"] = "application/json"
}
val requestPayload = mapOf(
"endpoint" to endpoint,
"method" to request.method(),
"headers" to headers
)
val requestData = JsonUtil.toJson(requestPayload).toByteArray()
val prefixData = "l${requestData.size}".toByteArray()
val suffixData = "e".toByteArray()
if (request.body() != null) {
val bodyPayload = mapOf(
"body" to body
)
val bodyData = JsonUtil.toJson(bodyPayload).toByteArray()
val bodyLengthData = "${bodyData.size}".toByteArray()
prefixData + requestData + bodyLengthData + bodyData + suffixData
} else {
prefixData + requestData + suffixData
}
} else {
val payload = mapOf(
"body" to body,
"endpoint" to endpoint,
"method" to request.method(),
"headers" to headers
)
JsonUtil.toJson(payload).toByteArray()
}
}
private fun handleResponse(
response: ByteArray,
destinationSymmetricKey: ByteArray,
destination: Destination,
version: Version,
deferred: Deferred<Map<*, *>, Exception>
) {
if (version == Version.V4) {
try {
if (response.size <= AESGCM.ivSize) return deferred.reject(Exception("Invalid response"))
// The data will be in the form of `l123:jsone` or `l123:json456:bodye` so we need to break the data into
// parts to properly process it
val plaintext = AESGCM.decrypt(response, destinationSymmetricKey)
val plaintextString = plaintext.decodeToString()
if (!plaintextString.startsWith("l")) return deferred.reject(Exception("Invalid response"))
val infoParts = plaintextString.split(":")
val infoLength = infoParts.firstOrNull()?.drop(1)?.toIntOrNull()
if (infoParts.size <= 1 || infoLength == null) return deferred.reject(Exception("Invalid response"))
val infoStartIndex = "l$infoLength".length + 1
val infoEndIndex = infoStartIndex + infoLength
val info = plaintextString.substring(infoStartIndex, infoEndIndex)
val responseInfo = JsonUtil.fromJson(info, Map::class.java)
when (val statusCode = responseInfo["code"].toString().toInt()) {
// Custom handle a clock out of sync error (v4 returns '425' but included the '406' just in case)
406, 425 -> {
@Suppress("NAME_SHADOWING")
val body =
mapOf("result" to "Your clock is out of sync with the service node network.")
val exception = HTTPRequestFailedAtDestinationException(
statusCode,
body,
destination.description
)
return deferred.reject(exception)
}
// Handle error status codes
!in 200..299 -> {
val exception = HTTPRequestFailedAtDestinationException(
statusCode,
responseInfo,
destination.description
)
return deferred.reject(exception)
}
}
// If there is no data in the response then just return the ResponseInfo
if (info.length < "l${infoLength}${info}e".length) {
return deferred.resolve(JsonUtil.fromJson(info, Map::class.java))
}
// Extract the response data as well
val data = plaintextString.substring(infoEndIndex)
val dataParts = data.split(":")
val dataLength = dataParts.firstOrNull()?.length
if (dataParts.size <= 1 || dataLength == null) return deferred.reject(Exception("Invalid JSON"))
val dataString = dataParts.last().dropLast(1)
return deferred.resolve(JsonUtil.fromJson(dataString, Map::class.java))
} catch (exception: Exception) {
deferred.reject(exception)
}
} else {
val bodyAsString = response.decodeToString()
val json = try {
JsonUtil.fromJson(bodyAsString, Map::class.java)
} catch (exception: Exception) {
mapOf( "result" to bodyAsString)
}
val base64EncodedIVAndCiphertext = json["result"] as? String ?: return deferred.reject(Exception("Invalid JSON"))
val ivAndCiphertext = Base64.decode(base64EncodedIVAndCiphertext)
try {
val plaintext = AESGCM.decrypt(ivAndCiphertext, destinationSymmetricKey)
try {
@Suppress("NAME_SHADOWING") val json =
JsonUtil.fromJson(plaintext.toString(Charsets.UTF_8), Map::class.java)
val statusCode = json["status_code"] as? Int ?: json["status"] as Int
when {
statusCode == 406 -> {
@Suppress("NAME_SHADOWING")
val body =
mapOf("result" to "Your clock is out of sync with the service node network.")
val exception = HTTPRequestFailedAtDestinationException(
statusCode,
body,
destination.description
)
return deferred.reject(exception)
}
json["body"] != null -> {
@Suppress("NAME_SHADOWING")
val body = if (json["body"] is Map<*, *>) {
json["body"] as Map<*, *>
} else {
val bodyAsString = json["body"] as String
JsonUtil.fromJson(bodyAsString, Map::class.java)
}
if (body["t"] != null) {
val timestamp = body["t"] as Long
val offset = timestamp - Date().time
SnodeAPI.clockOffset = offset
}
if (statusCode != 200) {
val exception = HTTPRequestFailedAtDestinationException(
statusCode,
body,
destination.description
)
return deferred.reject(exception)
}
deferred.resolve(body)
}
else -> {
if (statusCode != 200) {
val exception = HTTPRequestFailedAtDestinationException(
statusCode,
json,
destination.description
)
return deferred.reject(exception)
}
deferred.resolve(json)
}
}
} catch (exception: Exception) {
deferred.reject(Exception("Invalid JSON: ${plaintext.toString(Charsets.UTF_8)}."))
}
} catch (exception: Exception) {
deferred.reject(exception)
}
}
}
// endregion
enum class Version(val value: String) {
V2("/loki/v2/lsrpc"),
V3("/loki/v3/lsrpc"),
V4("/oxen/v4/lsrpc");
}
data class ResponseInfo(
val code: String,
val headers: Map<String, String>
)
}

View File

@ -2,6 +2,7 @@ package org.session.libsession.snode
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import org.session.libsession.snode.OnionRequestAPI.Destination
import org.session.libsession.utilities.AESGCM
import org.session.libsession.utilities.AESGCM.EncryptionResult
import org.session.libsignal.utilities.toHexString
@ -31,25 +32,29 @@ object OnionRequestEncryption {
/**
* Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
*/
internal fun encryptPayloadForDestination(payload: Map<*, *>, destination: OnionRequestAPI.Destination): Promise<EncryptionResult, Exception> {
internal fun encryptPayloadForDestination(
payload: ByteArray,
destination: Destination,
version: OnionRequestAPI.Version
): Promise<EncryptionResult, Exception> {
val deferred = deferred<EncryptionResult, Exception>()
ThreadUtils.queue {
try {
// Wrapping isn't needed for file server or open group onion requests
when (destination) {
is OnionRequestAPI.Destination.Snode -> {
val snodeX25519PublicKey = destination.snode.publicKeySet!!.x25519Key
val payloadAsData = JsonUtil.toJson(payload).toByteArray()
val plaintext = encode(payloadAsData, mapOf( "headers" to "" ))
val result = AESGCM.encrypt(plaintext, snodeX25519PublicKey)
deferred.resolve(result)
}
is OnionRequestAPI.Destination.Server -> {
val plaintext = JsonUtil.toJson(payload).toByteArray()
val result = AESGCM.encrypt(plaintext, destination.x25519PublicKey)
deferred.resolve(result)
val plaintext = if (version == OnionRequestAPI.Version.V4) {
payload
} else {
// Wrapping isn't needed for file server or open group onion requests
when (destination) {
is Destination.Snode -> encode(payload, mapOf("headers" to ""))
is Destination.Server -> payload
}
}
val x25519PublicKey = when (destination) {
is Destination.Snode -> destination.snode.publicKeySet!!.x25519Key
is Destination.Server -> destination.x25519PublicKey
}
val result = AESGCM.encrypt(plaintext, x25519PublicKey)
deferred.resolve(result)
} catch (exception: Exception) {
deferred.reject(exception)
}
@ -60,17 +65,16 @@ object OnionRequestEncryption {
/**
* Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
*/
internal fun encryptHop(lhs: OnionRequestAPI.Destination, rhs: OnionRequestAPI.Destination, previousEncryptionResult: EncryptionResult): Promise<EncryptionResult, Exception> {
internal fun encryptHop(lhs: Destination, rhs: Destination, previousEncryptionResult: EncryptionResult): Promise<EncryptionResult, Exception> {
val deferred = deferred<EncryptionResult, Exception>()
ThreadUtils.queue {
try {
val payload: MutableMap<String, Any>
when (rhs) {
is OnionRequestAPI.Destination.Snode -> {
payload = mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
val payload: MutableMap<String, Any> = when (rhs) {
is Destination.Snode -> {
mutableMapOf( "destination" to rhs.snode.publicKeySet!!.ed25519Key )
}
is OnionRequestAPI.Destination.Server -> {
payload = mutableMapOf(
is Destination.Server -> {
mutableMapOf(
"host" to rhs.host,
"target" to rhs.target,
"method" to "POST",
@ -80,13 +84,12 @@ object OnionRequestEncryption {
}
}
payload["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
val x25519PublicKey: String
when (lhs) {
is OnionRequestAPI.Destination.Snode -> {
x25519PublicKey = lhs.snode.publicKeySet!!.x25519Key
val x25519PublicKey = when (lhs) {
is Destination.Snode -> {
lhs.snode.publicKeySet!!.x25519Key
}
is OnionRequestAPI.Destination.Server -> {
x25519PublicKey = lhs.x25519PublicKey
is Destination.Server -> {
lhs.x25519PublicKey
}
}
val plaintext = encode(previousEncryptionResult.ciphertext, payload)

View File

@ -74,16 +74,23 @@ object SnodeAPI {
}
// Internal API
internal fun invoke(method: Snode.Method, snode: Snode, publicKey: String? = null, parameters: Map<String, Any>): RawResponsePromise {
internal fun invoke(
method: Snode.Method,
snode: Snode,
parameters: Map<String, Any>,
publicKey: String? = null,
version: OnionRequestAPI.Version = OnionRequestAPI.Version.V3
): RawResponsePromise {
val url = "${snode.address}:${snode.port}/storage_rpc/v1"
if (useOnionRequests) {
return OnionRequestAPI.sendOnionRequest(method, parameters, snode, publicKey)
return OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey)
} else {
val deferred = deferred<Map<*, *>, Exception>()
ThreadUtils.queue {
val payload = mapOf( "method" to method.rawValue, "params" to parameters )
try {
val json = HTTP.execute(HTTP.Verb.POST, url, payload)
val response = HTTP.execute(HTTP.Verb.POST, url, payload).toString()
val json = JsonUtil.fromJson(response, Map::class.java)
deferred.resolve(json)
} catch (exception: Exception) {
val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException
@ -117,7 +124,12 @@ object SnodeAPI {
deferred<Snode, Exception>()
ThreadUtils.queue {
try {
val json = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true)
val response = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true).toString()
val json = try {
JsonUtil.fromJson(response, Map::class.java)
} catch (exception: Exception) {
mapOf( "result" to response)
}
val intermediate = json["result"] as? Map<*, *>
val rawSnodes = intermediate?.get("service_node_states") as? List<*>
if (rawSnodes != null) {
@ -192,7 +204,7 @@ object SnodeAPI {
val promises = (1..validationCount).map {
getRandomSnode().bind { snode ->
retryIfNeeded(maxRetryCount) {
invoke(Snode.Method.OxenDaemonRPCCall, snode, null, parameters)
invoke(Snode.Method.OxenDaemonRPCCall, snode, parameters)
}
}
}
@ -268,7 +280,7 @@ object SnodeAPI {
} else {
val parameters = mapOf( "pubKey" to if (useTestnet) publicKey.removing05PrefixIfNeeded() else publicKey )
return getRandomSnode().bind {
invoke(Snode.Method.GetSwarm, it, publicKey, parameters)
invoke(Snode.Method.GetSwarm, it, parameters, publicKey)
}.map {
parseSnodes(it).toSet()
}.success {
@ -299,7 +311,7 @@ object SnodeAPI {
// "pubkey_ed25519" to ed25519PublicKey,
// "signature" to Base64.encodeBytes(signature)
)
return invoke(Snode.Method.GetMessages, snode, publicKey, parameters)
return invoke(Snode.Method.GetMessages, snode, parameters, publicKey)
}
fun getMessages(publicKey: String): MessageListPromise {
@ -311,7 +323,7 @@ object SnodeAPI {
}
private fun getNetworkTime(snode: Snode): Promise<Pair<Snode,Long>, Exception> {
return invoke(Snode.Method.Info, snode, null, emptyMap()).map { rawResponse ->
return invoke(Snode.Method.Info, snode, emptyMap()).map { rawResponse ->
val timestamp = rawResponse["timestamp"] as? Long ?: -1
snode to timestamp
}
@ -323,7 +335,7 @@ object SnodeAPI {
getTargetSnodes(destination).map { swarm ->
swarm.map { snode ->
val parameters = message.toJSON()
invoke(Snode.Method.SendMessage, snode, destination, parameters)
invoke(Snode.Method.SendMessage, snode, parameters, destination)
}.toSet()
}
}
@ -345,7 +357,7 @@ object SnodeAPI {
"messages" to serverHashes,
"signature" to Base64.encodeBytes(signature)
)
invoke(Snode.Method.DeleteMessage, snode, publicKey, deleteMessageParams).map { rawResponse ->
invoke(Snode.Method.DeleteMessage, snode, deleteMessageParams, publicKey).map { rawResponse ->
val swarms = rawResponse["swarm"] as? Map<String, Any> ?: return@map mapOf()
val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) ->
val json = rawJSON as? Map<String, Any> ?: return@mapNotNull null
@ -415,7 +427,7 @@ object SnodeAPI {
"timestamp" to timestamp,
"signature" to Base64.encodeBytes(signature)
)
invoke(Snode.Method.DeleteAll, snode, userPublicKey, deleteMessageParams).map {
invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey).map {
rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse)
}.fail { e ->
Log.e("Loki", "Failed to clear data", e)
@ -530,7 +542,7 @@ object SnodeAPI {
400, 500, 502, 503 -> { // Usually indicates that the snode isn't up to date
handleBadSnode()
}
425 -> {
406 -> {
Log.d("Loki", "The user's clock is out of sync with the service node network.")
broadcaster.broadcast("clockOutOfSync")
return Error.ClockOutOfSync

View File

@ -1,9 +1,8 @@
package org.session.libsession.utilities
import okhttp3.HttpUrl
import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsignal.utilities.Log
import org.session.libsignal.messages.SignalServiceAttachment
import java.io.*
object DownloadUtilities {
@ -37,7 +36,7 @@ object DownloadUtilities {
val url = HttpUrl.parse(urlAsString)!!
val fileID = url.pathSegments().last()
try {
FileServerAPIV2.download(fileID.toLong()).get().let {
FileServerApi.download(fileID.toLong()).get().let {
outputStream.write(it)
}
} catch (e: Exception) {

View File

@ -4,7 +4,7 @@ import android.content.Context
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.deferred
import okio.Buffer
import org.session.libsession.messaging.file_server.FileServerAPIV2
import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsignal.streams.ProfileCipherOutputStream
import org.session.libsignal.utilities.ProfileAvatarData
import org.session.libsignal.streams.DigestingRequestBody
@ -30,13 +30,13 @@ object ProfilePictureUtilities {
var id: Long = 0
try {
id = retryIfNeeded(4) {
FileServerAPIV2.upload(data)
FileServerApi.upload(data)
}.get()
} catch (e: Exception) {
deferred.reject(e)
}
TextSecurePreferences.setLastProfilePictureUpload(context, Date().time)
val url = "${FileServerAPIV2.server}/files/$id"
val url = "${FileServerApi.server}/files/$id"
TextSecurePreferences.setProfilePictureURL(context, url)
deferred.resolve(Unit)
}

View File

@ -1,7 +1,10 @@
package org.session.libsignal.utilities
import okhttp3.*
import java.lang.IllegalStateException
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
@ -68,26 +71,26 @@ object HTTP {
/**
* Sync. Don't call from the main thread.
*/
fun execute(verb: Verb, url: String, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> {
fun execute(verb: Verb, url: String, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray {
return execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
}
/**
* Sync. Don't call from the main thread.
*/
fun execute(verb: Verb, url: String, parameters: Map<String, Any>?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> {
if (parameters != null) {
fun execute(verb: Verb, url: String, parameters: Map<String, Any>?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray {
return if (parameters != null) {
val body = JsonUtil.toJson(parameters).toByteArray()
return execute(verb = verb, url = url, body = body, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
execute(verb = verb, url = url, body = body, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
} else {
return execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
execute(verb = verb, url = url, body = null, timeout = timeout, useSeedNodeConnection = useSeedNodeConnection)
}
}
/**
* Sync. Don't call from the main thread.
*/
fun execute(verb: Verb, url: String, body: ByteArray?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): Map<*, *> {
fun execute(verb: Verb, url: String, body: ByteArray?, timeout: Long = HTTP.timeout, useSeedNodeConnection: Boolean = false): ByteArray {
val request = Request.Builder().url(url)
.removeHeader("User-Agent").addHeader("User-Agent", "WhatsApp") // Set a fake value
.removeHeader("Accept-Language").addHeader("Accept-Language", "en-us") // Set a fake value
@ -103,14 +106,13 @@ object HTTP {
}
lateinit var response: Response
try {
val connection: OkHttpClient
if (timeout != HTTP.timeout) { // Custom timeout
val connection = if (timeout != HTTP.timeout) { // Custom timeout
if (useSeedNodeConnection) {
throw IllegalStateException("Setting a custom timeout is only allowed for requests to snodes.")
}
connection = getDefaultConnection(timeout)
getDefaultConnection(timeout)
} else {
connection = if (useSeedNodeConnection) seedNodeConnection else defaultConnection
if (useSeedNodeConnection) seedNodeConnection else defaultConnection
}
response = connection.newCall(request.build()).execute()
} catch (exception: Exception) {
@ -118,14 +120,9 @@ object HTTP {
// Override the actual error so that we can correctly catch failed requests in OnionRequestAPI
throw HTTPRequestFailedException(0, null)
}
when (val statusCode = response.code()) {
return when (val statusCode = response.code()) {
200 -> {
val bodyAsString = response.body()?.string() ?: throw Exception("An error occurred.")
try {
return JsonUtil.fromJson(bodyAsString, Map::class.java)
} catch (exception: Exception) {
return mapOf( "result" to bodyAsString)
}
response.body()?.bytes() ?: throw Exception("An error occurred.")
}
else -> {
Log.d("Loki", "${verb.rawValue} request to $url failed with status code: $statusCode.")