Merge branch 'dev' into release/1.19.0

This commit is contained in:
ThomasSession
2024-08-05 15:08:06 +10:00
committed by GitHub
109 changed files with 473 additions and 663 deletions

View File

@@ -12,11 +12,11 @@ import org.session.libsession.utilities.Util.equals
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsignal.streams.ProfileCipherInputStream
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.InputStream
import java.security.SecureRandom
import java.util.concurrent.ConcurrentSkipListSet
class RetrieveProfileAvatarJob(private val profileAvatar: String?, val recipientAddress: Address): Job {
@@ -64,7 +64,7 @@ class RetrieveProfileAvatarJob(private val profileAvatar: String?, val recipient
Log.w(TAG, "Removing profile avatar for: " + recipient.address.serialize())
if (recipient.isLocalNumber) {
setProfileAvatarId(context, SecureRandom().nextInt())
setProfileAvatarId(context, SECURE_RANDOM.nextInt())
setProfilePictureURL(context, null)
}
@@ -83,7 +83,7 @@ class RetrieveProfileAvatarJob(private val profileAvatar: String?, val recipient
decryptDestination.renameTo(AvatarHelper.getAvatarFile(context, recipient.address))
if (recipient.isLocalNumber) {
setProfileAvatarId(context, SecureRandom().nextInt())
setProfileAvatarId(context, SECURE_RANDOM.nextInt())
setProfilePictureURL(context, profileAvatar)
}

View File

@@ -10,7 +10,7 @@ import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveParameters
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.GroupUtil
import org.session.libsignal.crypto.getRandomElementOrNull
import org.session.libsignal.crypto.secureRandomOrNull
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.defaultRequiresAuth
@@ -104,7 +104,7 @@ class ClosedGroupPollerV2 {
fun poll(groupPublicKey: String): Promise<Unit, Exception> {
if (!isPolling(groupPublicKey)) { return Promise.of(Unit) }
val promise = SnodeAPI.getSwarm(groupPublicKey).bind { swarm ->
val snode = swarm.getRandomElementOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
val snode = swarm.secureRandomOrNull() ?: throw InsufficientSnodesException() // Should be cryptographically secure
if (!isPolling(groupPublicKey)) { throw PollingCanceledException() }
val currentForkInfo = SnodeAPI.forkInfo
when {

View File

@@ -19,8 +19,6 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveParameters
import org.session.libsession.messaging.messages.control.SharedConfigurationMessage
import org.session.libsession.messaging.sending_receiving.MessageReceiver
import org.session.libsession.snode.RawResponse
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeModule
@@ -29,7 +27,7 @@ import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode
import java.security.SecureRandom
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import java.util.Timer
import java.util.TimerTask
import kotlin.time.Duration.Companion.days
@@ -106,7 +104,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
val swarm = SnodeModule.shared.storage.getSwarm(userPublicKey) ?: setOf()
val unusedSnodes = swarm.subtract(usedSnodes)
if (unusedSnodes.isNotEmpty()) {
val index = SecureRandom().nextInt(unusedSnodes.size)
val index = SECURE_RANDOM.nextInt(unusedSnodes.size)
val nextSnode = unusedSnodes.elementAt(index)
usedSnodes.add(nextSnode)
Log.d(TAG, "Polling $nextSnode.")

View File

@@ -28,7 +28,7 @@ object MessageWrapper {
val webSocketMessage = createWebSocketMessage(envelope)
return webSocketMessage.toByteArray()
} catch (e: Exception) {
throw if (e is Error) { e } else { Error.FailedToWrapData }
throw if (e is Error) e else Error.FailedToWrapData
}
}
@@ -49,15 +49,15 @@ object MessageWrapper {
private fun createWebSocketMessage(envelope: Envelope): WebSocketMessage {
try {
val requestBuilder = WebSocketRequestMessage.newBuilder()
requestBuilder.verb = "PUT"
requestBuilder.path = "/api/v1/message"
requestBuilder.id = SecureRandom.getInstance("SHA1PRNG").nextLong()
requestBuilder.body = envelope.toByteString()
val messageBuilder = WebSocketMessage.newBuilder()
messageBuilder.request = requestBuilder.build()
messageBuilder.type = WebSocketMessage.Type.REQUEST
return messageBuilder.build()
return WebSocketMessage.newBuilder().apply {
request = WebSocketRequestMessage.newBuilder().apply {
verb = "PUT"
path = "/api/v1/message"
id = SecureRandom.getInstance("SHA1PRNG").nextLong()
body = envelope.toByteString()
}.build()
type = WebSocketMessage.Type.REQUEST
}.build()
} catch (e: Exception) {
Log.d("Loki", "Failed to wrap envelope in web socket message: ${e.message}.")
throw Error.FailedToWrapEnvelopeInWebSocketMessage

View File

@@ -12,8 +12,8 @@ import org.session.libsession.utilities.AESGCM
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.crypto.secureRandom
import org.session.libsignal.crypto.secureRandomOrNull
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Broadcaster
@@ -148,7 +148,7 @@ object OnionRequestAPI {
val reusableGuardSnodeCount = reusableGuardSnodes.count()
if (unusedSnodes.count() < (targetGuardSnodeCount - reusableGuardSnodeCount)) { throw InsufficientSnodesException() }
fun getGuardSnode(): Promise<Snode, Exception> {
val candidate = unusedSnodes.getRandomElementOrNull()
val candidate = unusedSnodes.secureRandomOrNull()
?: return Promise.ofFail(InsufficientSnodesException())
unusedSnodes = unusedSnodes.minus(candidate)
Log.d("Loki", "Testing guard snode: $candidate.")
@@ -189,8 +189,10 @@ 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.secureRandom()
// remove the snode from the unused list and return it
unusedSnodes = unusedSnodes.minus(pathSnode)
pathSnode
}
@@ -225,9 +227,9 @@ object OnionRequestAPI {
OnionRequestAPI.guardSnodes = guardSnodes
fun getPath(paths: List<Path>): Path {
return if (snodeToExclude != null) {
paths.filter { !it.contains(snodeToExclude) }.getRandomElement()
paths.filter { !it.contains(snodeToExclude) }.secureRandom()
} else {
paths.getRandomElement()
paths.secureRandom()
}
}
when {
@@ -270,7 +272,7 @@ object OnionRequestAPI {
path.removeAt(snodeIndex)
val unusedSnodes = SnodeAPI.snodePool.minus(oldPaths.flatten())
if (unusedSnodes.isEmpty()) { throw InsufficientSnodesException() }
path.add(unusedSnodes.getRandomElement())
path.add(unusedSnodes.secureRandom())
// Don't test the new snode as this would reveal the user's IP
oldPaths.removeAt(pathIndex)
val newPaths = oldPaths + listOf( path )

View File

@@ -18,7 +18,8 @@ import nl.komponents.kovenant.task
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.utilities.MessageWrapper
import org.session.libsession.messaging.utilities.SodiumUtilities.sodium
import org.session.libsignal.crypto.getRandomElement
import org.session.libsignal.crypto.secureRandom
import org.session.libsignal.crypto.shuffledRandom
import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.protos.SignalServiceProtos
import org.session.libsignal.utilities.Base64
@@ -30,6 +31,7 @@ import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode
import org.session.libsignal.utilities.ThreadUtils
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import org.session.libsignal.utilities.prettifiedDescription
import org.session.libsignal.utilities.retryIfNeeded
import java.security.SecureRandom
@@ -88,6 +90,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
sealed class Error(val description: String) : Exception(description) {
object Generic : Error("An error occurred.")
@@ -146,6 +156,7 @@ object SnodeAPI {
internal fun getRandomSnode(): Promise<Snode, Exception> {
val snodePool = this.snodePool
if (snodePool.count() < minimumSnodePoolCount) {
val target = seedNodePool.random()
val url = "$target/json_rpc"
@@ -154,8 +165,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<Snode, Exception>()
@@ -173,12 +187,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<Int>() // 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
@@ -187,7 +211,7 @@ object SnodeAPI {
Log.d("Loki", "Persisting snode pool to database.")
this.snodePool = snodePool
try {
deferred.resolve(snodePool.getRandomElement())
deferred.resolve(snodePool.secureRandom())
} catch (exception: Exception) {
Log.d("Loki", "Got an empty snode pool from: $target.")
deferred.reject(SnodeAPI.Error.Generic)
@@ -202,10 +226,14 @@ object SnodeAPI {
}
return deferred.promise
} else {
return Promise.of(snodePool.getRandomElement())
return Promise.of(snodePool.secureRandom())
}
}
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)) {
@@ -215,8 +243,8 @@ object SnodeAPI {
}
internal fun getSingleTargetSnode(publicKey: String): Promise<Snode, Exception> {
// SecureRandom() should be cryptographically secure
return getSwarm(publicKey).map { it.shuffled(SecureRandom()).random() }
// SecureRandom should be cryptographically secure
return getSwarm(publicKey).map { it.shuffledRandom().random() }
}
// Public API
@@ -716,10 +744,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

View File

@@ -13,6 +13,7 @@ import android.text.TextUtils
import android.text.style.StyleSpan
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import java.io.*
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
@@ -292,15 +293,10 @@ object Util {
@JvmStatic
fun getSecretBytes(size: Int): ByteArray {
val secret = ByteArray(size)
getSecureRandom().nextBytes(secret)
SECURE_RANDOM.nextBytes(secret)
return secret
}
@JvmStatic
fun getSecureRandom(): SecureRandom {
return SecureRandom()
}
@JvmStatic
fun getFirstNonEmpty(vararg values: String?): String? {
for (value in values) {
@@ -317,18 +313,14 @@ object Util {
}
@JvmStatic
fun <T> getRandomElement(elements: Array<T>): T {
return elements[SecureRandom().nextInt(elements.size)]
}
fun <T> getRandomElement(elements: Array<T>): T = elements[SECURE_RANDOM.nextInt(elements.size)]
@JvmStatic
fun getBoldedString(value: String?): CharSequence {
if (value.isNullOrEmpty()) { return "" }
val spanned = SpannableString(value)
spanned.setSpan(StyleSpan(Typeface.BOLD), 0,
spanned.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spanned
return SpannableString(value).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, it.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
@JvmStatic
@@ -366,6 +358,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, R> T.runIf(condition: Boolean, block: T.() -> R): R where T: R = if (condition) block() else this

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="enable_alarm_manager">true</bool>
<bool name="enable_job_service">false</bool>
<bool name="screen_security_default">true</bool>
</resources>