mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-11 11:57:24 +00:00
Merge branch 'dev' into release/1.19.0
This commit is contained in:
@@ -2,6 +2,8 @@ plugins {
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
id 'kotlinx-serialization'
|
||||
id 'com.google.devtools.ksp'
|
||||
id 'com.google.dagger.hilt.android'
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -21,6 +23,11 @@ dependencies {
|
||||
implementation project(":libsignal")
|
||||
implementation project(":libsession-util")
|
||||
implementation project(":liblazysodium")
|
||||
|
||||
implementation("com.google.dagger:hilt-android:$daggerHiltVersion")
|
||||
ksp("com.google.dagger:hilt-compiler:$daggerHiltVersion")
|
||||
ksp("androidx.hilt:hilt-compiler:$jetpackHiltVersion")
|
||||
|
||||
implementation "net.java.dev.jna:jna:5.12.1@aar"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
|
||||
implementation "androidx.core:core-ktx:$coreVersion"
|
||||
@@ -28,7 +35,6 @@ dependencies {
|
||||
implementation "androidx.preference:preference-ktx:$preferenceVersion"
|
||||
implementation "com.google.android.material:material:$materialVersion"
|
||||
implementation "com.google.protobuf:protobuf-java:$protobufVersion"
|
||||
implementation "com.google.dagger:hilt-android:$daggerVersion"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
|
||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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.")
|
||||
|
@@ -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
|
||||
|
@@ -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 )
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user