From 79ae27d7b696b107373cf25f0c34623f472e1a42 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 23 Jul 2024 17:52:53 +1000 Subject: [PATCH] Snode version patch (#1561) * Snode version number Getting the version number from the API and checking the last node in the onion routing, making sure its version is at least 2.8.0 * Clearing the snode and onion request dbs on launch * Removing logs * Tweak to snode filtering * PR feedback --- .../securesms/ApplicationContext.java | 11 +++++ .../securesms/database/LokiAPIDatabase.kt | 18 +++++-- .../libsession/snode/OnionRequestAPI.kt | 18 ++++++- .../org/session/libsession/snode/SnodeAPI.kt | 49 ++++++++++++++----- .../utilities/TextSecurePreferences.kt | 12 +++++ .../org/session/libsession/utilities/Util.kt | 28 +++++++++++ .../database/LokiAPIDatabaseProtocol.kt | 1 + .../org/session/libsignal/utilities/Snode.kt | 2 +- 8 files changed, 122 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 03b56d6b61..975d12c8e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -214,6 +214,17 @@ public class ApplicationContext extends Application implements DefaultLifecycleO DatabaseModule.init(this); MessagingModuleConfiguration.configure(this); super.onCreate(); + + // we need to clear the snode and onionrequest databases once on first launch + // in order to apply a patch that adds a version number to the Snode objects. + if(!TextSecurePreferences.hasAppliedPatchSnodeVersion(this)) { + ThreadUtils.queue(() -> { + lokiAPIDatabase.clearSnodePool(); + lokiAPIDatabase.clearOnionRequestPaths(); + TextSecurePreferences.setHasAppliedPatchSnodeVersion(this, true); + }); + } + messagingModuleConfiguration = new MessagingModuleConfiguration( this, storage, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt index f60c53bbe3..f1f999242c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt @@ -166,6 +166,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( const val RESET_SEQ_NO = "UPDATE $lastMessageServerIDTable SET $lastMessageServerID = 0;" + const val EMPTY_VERSION = "0.0.0" + // endregion } @@ -179,7 +181,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null val x25519Key = components.getOrNull(3) ?: return@mapNotNull null - Snode(address, port, Snode.KeySet(ed25519Key, x25519Key)) + val version = components.getOrNull(4) ?: EMPTY_VERSION + Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version) } }?.toSet() ?: setOf() } @@ -192,6 +195,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( if (keySet != null) { string += "-${keySet.ed25519Key}-${keySet.x25519Key}" } + string += "-${snode.version}" string } val row = wrap(mapOf( Companion.dummyKey to "dummy_key", snodePool to snodePoolAsString )) @@ -207,6 +211,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( if (keySet != null) { snodeAsString += "-${keySet.ed25519Key}-${keySet.x25519Key}" } + snodeAsString += "-${snode.version}" val row = wrap(mapOf( Companion.indexPath to indexPath, Companion.snode to snodeAsString )) database.insertOrUpdate(onionRequestPathTable, row, "${Companion.indexPath} = ?", wrap(indexPath)) } @@ -232,8 +237,9 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( val port = components.getOrNull(1)?.toIntOrNull() val ed25519Key = components.getOrNull(2) val x25519Key = components.getOrNull(3) + val version = components.getOrNull(4) ?: EMPTY_VERSION if (port != null && ed25519Key != null && x25519Key != null) { - Snode(address, port, Snode.KeySet(ed25519Key, x25519Key)) + Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version) } else { null } @@ -251,6 +257,11 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( return result } + override fun clearSnodePool() { + val database = databaseHelper.writableDatabase + database.delete(snodePoolTable, null, null) + } + override fun clearOnionRequestPaths() { val database = databaseHelper.writableDatabase fun delete(indexPath: String) { @@ -271,7 +282,8 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null val x25519Key = components.getOrNull(3) ?: return@mapNotNull null - Snode(address, port, Snode.KeySet(ed25519Key, x25519Key)) + val version = components.getOrNull(4) ?: EMPTY_VERSION + Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version) } }?.toSet() } diff --git a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt index 04b0f722c1..fb3522969b 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -10,6 +10,7 @@ import okhttp3.Request import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsession.utilities.AESGCM import org.session.libsession.utilities.AESGCM.EncryptionResult +import org.session.libsession.utilities.Util import org.session.libsession.utilities.getBodyForOnionRequest import org.session.libsession.utilities.getHeadersForOnionRequest import org.session.libsignal.crypto.getRandomElement @@ -190,8 +191,21 @@ object OnionRequestAPI { if (unusedSnodes.count() < pathSnodeCount) { throw InsufficientSnodesException() } // Don't test path snodes as this would reveal the user's IP to them guardSnodes.minus(reusableGuardSnodes).map { guardSnode -> - val result = listOf( guardSnode ) + (0 until (pathSize - 1)).map { - val pathSnode = unusedSnodes.getRandomElement() + val result = listOf( guardSnode ) + (0 until (pathSize - 1)).mapIndexed() { index, _ -> + var pathSnode = unusedSnodes.getRandomElement() + + // For the last node: We need to make sure the version is >= 2.8.0 + // to help with an issue that will disappear once the nodes are all updated + if(index == pathSize - 2) { + val suitableSnodes = unusedSnodes.filter { Util.compareVersions(it.version, "2.8.0") >= 0 } + pathSnode = if (suitableSnodes.isNotEmpty()) { + suitableSnodes.random() + } else { + throw InsufficientSnodesException() + } + } + + // remove the snode from the unused list and return it unusedSnodes = unusedSnodes.minus(pathSnode) pathSnode } diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index d2cfa2de35..9b008fad06 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -88,6 +88,14 @@ object SnodeAPI { const val useTestnet = false + const val KEY_IP = "public_ip" + const val KEY_PORT = "storage_port" + const val KEY_X25519 = "pubkey_x25519" + const val KEY_ED25519 = "pubkey_ed25519" + const val KEY_VERSION = "storage_server_version" + + const val EMPTY_VERSION = "0.0.0" + // Error internal sealed class Error(val description: String) : Exception(description) { object Generic : Error("An error occurred.") @@ -146,6 +154,7 @@ object SnodeAPI { internal fun getRandomSnode(): Promise { val snodePool = this.snodePool + if (snodePool.count() < minimumSnodePoolCount) { val target = seedNodePool.random() val url = "$target/json_rpc" @@ -154,8 +163,11 @@ object SnodeAPI { "method" to "get_n_service_nodes", "params" to mapOf( "active_only" to true, - "limit" to 256, - "fields" to mapOf("public_ip" to true, "storage_port" to true, "pubkey_x25519" to true, "pubkey_ed25519" to true) + "fields" to mapOf( + KEY_IP to true, KEY_PORT to true, + KEY_X25519 to true, KEY_ED25519 to true, + KEY_VERSION to true + ) ) ) val deferred = deferred() @@ -173,12 +185,22 @@ object SnodeAPI { if (rawSnodes != null) { val snodePool = rawSnodes.mapNotNull { rawSnode -> val rawSnodeAsJSON = rawSnode as? Map<*, *> - val address = rawSnodeAsJSON?.get("public_ip") as? String - val port = rawSnodeAsJSON?.get("storage_port") as? Int - val ed25519Key = rawSnodeAsJSON?.get("pubkey_ed25519") as? String - val x25519Key = rawSnodeAsJSON?.get("pubkey_x25519") as? String - if (address != null && port != null && ed25519Key != null && x25519Key != null && address != "0.0.0.0") { - Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key)) + val address = rawSnodeAsJSON?.get(KEY_IP) as? String + val port = rawSnodeAsJSON?.get(KEY_PORT) as? Int + val ed25519Key = rawSnodeAsJSON?.get(KEY_ED25519) as? String + val x25519Key = rawSnodeAsJSON?.get(KEY_X25519) as? String + val version = (rawSnodeAsJSON?.get(KEY_VERSION) as? ArrayList<*>) + ?.filterIsInstance() // get the array as Integers + ?.joinToString(separator = ".") // turn it int a version string + + if (address != null && port != null && ed25519Key != null && x25519Key != null + && address != "0.0.0.0" && version != null) { + Snode( + address = "https://$address", + port = port, + publicKeySet = Snode.KeySet(ed25519Key, x25519Key), + version = version + ) } else { Log.d("Loki", "Failed to parse: ${rawSnode?.prettifiedDescription()}.") null @@ -206,6 +228,10 @@ object SnodeAPI { } } + private fun extractVersionString(jsonVersion: String): String{ + return jsonVersion.removeSurrounding("[", "]").split(", ").joinToString(separator = ".") + } + internal fun dropSnodeFromSwarmIfNeeded(snode: Snode, publicKey: String) { val swarm = database.getSwarm(publicKey)?.toMutableSet() if (swarm != null && swarm.contains(snode)) { @@ -716,10 +742,11 @@ object SnodeAPI { val address = rawSnodeAsJSON?.get("ip") as? String val portAsString = rawSnodeAsJSON?.get("port") as? String val port = portAsString?.toInt() - val ed25519Key = rawSnodeAsJSON?.get("pubkey_ed25519") as? String - val x25519Key = rawSnodeAsJSON?.get("pubkey_x25519") as? String + val ed25519Key = rawSnodeAsJSON?.get(KEY_ED25519) as? String + val x25519Key = rawSnodeAsJSON?.get(KEY_X25519) as? String + if (address != null && port != null && ed25519Key != null && x25519Key != null && address != "0.0.0.0") { - Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key)) + Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key), EMPTY_VERSION) } else { Log.d("Loki", "Failed to parse snode from: ${rawSnode?.prettifiedDescription()}.") null diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index af16d93f5a..2a92689aab 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -292,6 +292,8 @@ interface TextSecurePreferences { const val ALLOW_MESSAGE_REQUESTS = "libsession.ALLOW_MESSAGE_REQUESTS" + const val PATCH_SNODE_VERSION_2024_07_23 = "libsession.patch_snode_version_2024_07_23" + @JvmStatic fun getLastConfigurationSyncTime(context: Context): Long { return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0) @@ -1013,6 +1015,16 @@ interface TextSecurePreferences { fun clearAll(context: Context) { getDefaultSharedPreferences(context).edit().clear().commit() } + + @JvmStatic + fun hasAppliedPatchSnodeVersion(context: Context): Boolean { + return getBooleanPreference(context, PATCH_SNODE_VERSION_2024_07_23, false) + } + + @JvmStatic + fun setHasAppliedPatchSnodeVersion(context: Context, applied: Boolean) { + setBooleanPreference(context, PATCH_SNODE_VERSION_2024_07_23, applied) + } } } diff --git a/libsession/src/main/java/org/session/libsession/utilities/Util.kt b/libsession/src/main/java/org/session/libsession/utilities/Util.kt index 17009caa7d..e842c54bee 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/Util.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/Util.kt @@ -365,6 +365,34 @@ object Util { val digitGroups = (Math.log10(sizeBytes.toDouble()) / Math.log10(1024.0)).toInt() return DecimalFormat("#,##0.#").format(sizeBytes / Math.pow(1024.0, digitGroups.toDouble())) + " " + units[digitGroups] } + + /** + * Compares two version strings (for example "1.8.0") + * + * @param version1 the first version string to compare. + * @param version2 the second version string to compare. + * @return an integer indicating the result of the comparison: + * - 0 if the versions are equal + * - a positive number if version1 is greater than version2 + * - a negative number if version1 is less than version2 + */ + @JvmStatic + fun compareVersions(version1: String, version2: String): Int { + val parts1 = version1.split(".").map { it.toIntOrNull() ?: 0 } + val parts2 = version2.split(".").map { it.toIntOrNull() ?: 0 } + + val maxLength = maxOf(parts1.size, parts2.size) + val paddedParts1 = parts1 + List(maxLength - parts1.size) { 0 } + val paddedParts2 = parts2 + List(maxLength - parts2.size) { 0 } + + for (i in 0 until maxLength) { + val compare = paddedParts1[i].compareTo(paddedParts2[i]) + if (compare != 0) { + return compare + } + } + return 0 + } } fun T.runIf(condition: Boolean, block: T.() -> R): R where T: R = if (condition) block() else this diff --git a/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt b/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt index 37c00a037d..d7458ff651 100644 --- a/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt +++ b/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt @@ -10,6 +10,7 @@ interface LokiAPIDatabaseProtocol { fun getSnodePool(): Set fun setSnodePool(newValue: Set) fun getOnionRequestPaths(): List> + fun clearSnodePool() fun clearOnionRequestPaths() fun setOnionRequestPaths(newValue: List>) fun getSwarm(publicKey: String): Set? diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt index 28f8aeb03b..f6b11754ad 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt @@ -1,6 +1,6 @@ package org.session.libsignal.utilities -class Snode(val address: String, val port: Int, val publicKeySet: KeySet?) { +class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val version: String) { val ip: String get() = address.removePrefix("https://") public enum class Method(val rawValue: String) {