Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/blinded version #1583

Merged
merged 9 commits into from
Jul 30, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
import org.thoughtcrime.securesms.util.Broadcaster;
import org.thoughtcrime.securesms.util.VersionUtil;
import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory;
Expand Down Expand Up @@ -142,6 +143,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private HandlerThread conversationListHandlerThread;
private Handler conversationListHandler;
private PersistentLogger persistentLogger;
private VersionUtil versionUtil;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could probably inject without much trouble


@Inject LokiAPIDatabase lokiAPIDatabase;
@Inject public Storage storage;
Expand Down Expand Up @@ -248,6 +250,7 @@ public void onCreate() {
resubmitProfilePictureIfNeeded();
loadEmojiSearchIndexIfNeeded();
EmojiSource.refresh();
versionUtil = new VersionUtil(textSecurePreferences);

NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
Expand All @@ -274,6 +277,9 @@ public void onStart(@NonNull LifecycleOwner owner) {

OpenGroupManager.INSTANCE.startPolling();
});

// fetch last version data
versionUtil.startTimedVersionCheck();
}

@Override
Expand All @@ -286,12 +292,14 @@ public void onStop(@NonNull LifecycleOwner owner) {
poller.stopIfNeeded();
}
ClosedGroupPollerV2.getShared().stopAll();
versionUtil.stopTimedVersionCheck();
}

@Override
public void onTerminate() {
stopKovenant(); // Loki
OpenGroupManager.INSTANCE.stopPolling();
versionUtil.clear();
super.onTerminate();
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ fun showMuteDialog(

private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) {
ONE_HOUR(R.string.arrays__mute_for_one_hour, duration = TimeUnit.HOURS.toMillis(1)),
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.DAYS.toMillis(2)),
TWO_HOURS(R.string.arrays__mute_for_two_hours, duration = TimeUnit.HOURS.toMillis(2)),
mpretty-cyro marked this conversation as resolved.
Show resolved Hide resolved
ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)),
SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static ParcelFileDescriptor getParcelFileDescriptor(MemoryFile file) thro

int fd = field.getInt(fileDescriptor);

return ParcelFileDescriptor.adoptFd(fd);
return ParcelFileDescriptor.fromFd(fd);
} catch (IllegalAccessException e) {
throw new IOException(e);
} catch (InvocationTargetException e) {
Expand Down
64 changes: 64 additions & 0 deletions app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.thoughtcrime.securesms.util

import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsession.utilities.TextSecurePreferences
import java.util.concurrent.TimeUnit

class VersionUtil(
private val prefs: TextSecurePreferences
) {
private val FOUR_HOURS: Long = TimeUnit.HOURS.toMillis(4)

private val handler = Handler(Looper.getMainLooper())
private val runnable: Runnable

private val scope = CoroutineScope(Dispatchers.Default)
private var job: Job? = null

init {
runnable = Runnable {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline

fetchAndScheduleNextVersionCheck()
}
}

fun startTimedVersionCheck() {
handler.post(runnable)
}

fun stopTimedVersionCheck() {
handler.removeCallbacks(runnable)
}

fun clear() {
job?.cancel()
stopTimedVersionCheck()
}

private fun fetchAndScheduleNextVersionCheck() {
fetchVersionData()
handler.postDelayed(runnable, FOUR_HOURS)
}

private fun fetchVersionData() {
// only perform this if at least 4h has elapsed since th last successful check
val lastCheck = System.currentTimeMillis() - prefs.getLastVersionCheck()
if(lastCheck < FOUR_HOURS) return
ThomasSession marked this conversation as resolved.
Show resolved Hide resolved

job?.cancel()
job = scope.launch {
try {
// perform the version check
val clientVersion = FileServerApi.getClientVersion()
prefs.setLastVersionCheck()
} catch (e: Exception) {
// we can silently ignore the error
}
}
}
}
1 change: 1 addition & 0 deletions libsession-util/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ set(SOURCES
config_base.cpp
contacts.cpp
conversation.cpp
blinded_key.cpp
util.cpp)

add_library( # Sets the name of the library.
Expand Down
34 changes: 34 additions & 0 deletions libsession-util/src/main/cpp/blinded_key.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include <jni.h>
#include <session/blinding.hpp>

#include "util.h"
#include "jni_utils.h"

//
// Created by Thomas Ruffie on 29/7/2024.
//

extern "C"
JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionKeyPair(JNIEnv *env,
jobject thiz,
jbyteArray ed25519_secret_key) {
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
const auto [pk, sk] = session::blind_version_key_pair(util::ustring_from_bytes(env, ed25519_secret_key));

jclass kp_class = env->FindClass("network/loki/messenger/libsession_util/util/KeyPair");
jmethodID kp_constructor = env->GetMethodID(kp_class, "<init>", "([B[B)V");
return env->NewObject(kp_class, kp_constructor, util::bytes_from_ustring(env, {pk.data(), pk.size()}), util::bytes_from_ustring(env, {sk.data(), sk.size()}));
});
}
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionSign(JNIEnv *env,
jobject thiz,
jbyteArray ed25519_secret_key,
jlong timestamp) {
return jni_utils::run_catching_cxx_exception_or_throws<jbyteArray>(env, [=] {
auto bytes = session::blind_version_sign(util::ustring_from_bytes(env, ed25519_secret_key), session::Platform::android, timestamp);
return util::bytes_from_ustring(env, bytes);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package network.loki.messenger.libsession_util.util

object BlindKeyAPI {
init {
System.loadLibrary("session_util")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would perhaps be ideal to just have one by lazy somewhere that we reference instead of loading this thing for the 8th time.

IDK if it's optimised away sometimes/always/never, but at least when we loaded a lib way to many times, it crashed on some devices.

}

external fun blindVersionKeyPair(ed25519SecretKey: ByteArray): KeyPair
external fun blindVersionSign(ed25519SecretKey: ByteArray, timestamp: Long): ByteArray
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package org.session.libsession.messaging.file_server

import android.util.Base64
import network.loki.messenger.libsession_util.util.BlindKeyAPI
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.utilities.await
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import java.util.concurrent.TimeUnit

object FileServerApi {

Expand All @@ -23,6 +27,7 @@ object FileServerApi {
sealed class Error(message: String) : Exception(message) {
object ParsingFailed : Error("Invalid response.")
object InvalidURL : Error("Invalid URL.")
object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
}

data class Request(
Expand Down Expand Up @@ -105,4 +110,52 @@ object FileServerApi {
val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file")
return send(request)
}

/**
* Returns the current version of session
* This is effectively proxying (and caching) the response from the github release
* page.
*
* Note that the value is cached and can be up to 30 minutes out of date normally, and up to 24
* hours out of date if we cannot reach the Github API for some reason.
*
* https://github.com/oxen-io/session-file-server/blob/dev/doc/api.yaml#L119
*/
suspend fun getClientVersion(): VersionData {
// Generate the auth signature
val secretKey = MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes
?: throw (Error.NoEd25519KeyPair)

val blindedKeys = BlindKeyAPI.blindVersionKeyPair(secretKey)
val timestamp = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) // The current timestamp in seconds

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so... you could perhaps more clearly:

val timestampSeconds = System.currentTimeMillis().milliseconds.inWholeSeconds

as it's useful to put units in the variable name if it is not contained in the type.

alternatively

val timestamp = System.currentTimeMillis().milliseconds
val signature = BlindKeyAPI.blindVersionSign(secretKey, timestamp.inWholeSeconds)
...
"X-FS-Timestamp" to timestamp.inWholeSeconds.toString(),

val signature = BlindKeyAPI.blindVersionSign(secretKey, timestamp)

// The hex encoded version-blinded public key with a 07 prefix
val blindedPkHex = buildString {
append("07")
append(blindedKeys.pubKey.toHexString())
}

val request = Request(
verb = HTTP.Verb.GET,
endpoint = "session_version",
queryParameters = mapOf("platform" to "android"),
headers = mapOf(
"X-FS-Pubkey" to blindedPkHex,
"X-FS-Timestamp" to timestamp.toString(),
"X-FS-Signature" to Base64.encodeToString(signature, Base64.NO_WRAP)
)
)

// transform the promise into a coroutine
val result = send(request).await()

// map out the result
val json = JsonUtil.fromJson(result, Map::class.java)
val statusCode = json.getOrDefault("status_code", 0) as Int
val version = json.getOrDefault("result", "") as String
val updated = json.getOrDefault("updated", 0.0) as Double

return VersionData(statusCode, version, updated)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing as there's all of these named vals that are simple enough to inline, they may as well be named params, and you get a little bit more safety that they're in the right order etc.

Suggested change
val json = JsonUtil.fromJson(result, Map::class.java)
val statusCode = json.getOrDefault("status_code", 0) as Int
val version = json.getOrDefault("result", "") as String
val updated = json.getOrDefault("updated", 0.0) as Double
return VersionData(statusCode, version, updated)
return JsonUtil.fromJson(result, Map::class.java).let {
VersionData(
statusCode = it["status_code"] as? Int ?: 0
version = it["result"] as? String ?: ""
updated = it["updated"] as? Double ?: 0.0
)
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.session.libsession.messaging.file_server

data class VersionData(
val statusCode: Int, // The value 200. Included for backwards compatibility, and may be removed someday.
val version: String, // The Session version.
val updated: Double // The unix timestamp when this version was retrieved from Github; this can be up to 24 hours ago in case of consistent fetch errors, though normally will be within the last 30 minutes.
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.session.libsession.snode.utilities

import nl.komponents.kovenant.Promise
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

suspend fun <T, E: Throwable> Promise<T, E>.await(): T {
return suspendCoroutine { cont ->
success { cont.resume(it) }
fail { cont.resumeWithException(it) }
ThomasSession marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import org.session.libsession.R
import org.session.libsession.utilities.TextSecurePreferences.Companion
import org.session.libsession.utilities.TextSecurePreferences.Companion.AUTOPLAY_AUDIO_MESSAGES
import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED
import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_DARK
import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_LIGHT
import org.session.libsession.utilities.TextSecurePreferences.Companion.FOLLOW_SYSTEM_SETTINGS
import org.session.libsession.utilities.TextSecurePreferences.Companion.HIDE_PASSWORD
import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VACUUM_TIME
import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VERSION_CHECK
import org.session.libsession.utilities.TextSecurePreferences.Companion.LEGACY_PREF_KEY_SELECTED_UI_MODE
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_DARK
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_LIGHT
Expand Down Expand Up @@ -186,6 +188,8 @@ interface TextSecurePreferences {
fun clearAll()
fun getHidePassword(): Boolean
fun setHidePassword(value: Boolean)
fun getLastVersionCheck(): Long
fun setLastVersionCheck()

companion object {
val TAG = TextSecurePreferences::class.simpleName
Expand Down Expand Up @@ -272,6 +276,7 @@ interface TextSecurePreferences {
const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio"
const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated"
const val SELECTED_ACCENT_COLOR = "selected_accent_color"
const val LAST_VERSION_CHECK = "pref_last_version_check"

const val HAS_RECEIVED_LEGACY_CONFIG = "has_received_legacy_config"
const val HAS_FORCED_NEW_CONFIG = "has_forced_new_config"
Expand Down Expand Up @@ -1541,6 +1546,14 @@ class AppTextSecurePreferences @Inject constructor(
setLongPreference(LAST_VACUUM_TIME, System.currentTimeMillis())
}

override fun getLastVersionCheck(): Long {
return getLongPreference(LAST_VERSION_CHECK, 0)
}

override fun setLastVersionCheck() {
setLongPreference(LAST_VERSION_CHECK, System.currentTimeMillis())
}

override fun setShownCallNotification(): Boolean {
val previousValue = getBooleanPreference(SHOWN_CALL_NOTIFICATION, false)
if (previousValue) return false
Expand Down