diff --git a/app/build.gradle b/app/build.gradle
index 2e52adddd5..772ca6ded5 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,7 +31,7 @@ configurations.all {
exclude module: "commons-logging"
}
-def canonicalVersionCode = 374
+def canonicalVersionCode = 375
def canonicalVersionName = "1.18.5"
def postFixSize = 10
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 79d55b37f8..b564f10dfd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -60,7 +60,6 @@
-
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) {