Merge pull request #1583 from oxen-io/feature/blinded-version

Feature/blinded version
This commit is contained in:
ThomasSession 2024-07-30 15:42:07 +10:00 committed by GitHub
commit fc4bf6ff27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 217 additions and 4 deletions

View File

@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager; import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository; import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
import org.thoughtcrime.securesms.util.Broadcaster; import org.thoughtcrime.securesms.util.Broadcaster;
import org.thoughtcrime.securesms.util.VersionUtil;
import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper; import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor; import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory;
@ -142,6 +143,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
private HandlerThread conversationListHandlerThread; private HandlerThread conversationListHandlerThread;
private Handler conversationListHandler; private Handler conversationListHandler;
private PersistentLogger persistentLogger; private PersistentLogger persistentLogger;
private VersionUtil versionUtil;
@Inject LokiAPIDatabase lokiAPIDatabase; @Inject LokiAPIDatabase lokiAPIDatabase;
@Inject public Storage storage; @Inject public Storage storage;
@ -248,6 +250,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
resubmitProfilePictureIfNeeded(); resubmitProfilePictureIfNeeded();
loadEmojiSearchIndexIfNeeded(); loadEmojiSearchIndexIfNeeded();
EmojiSource.refresh(); EmojiSource.refresh();
versionUtil = new VersionUtil(textSecurePreferences);
NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create(); NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create();
HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet); HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet);
@ -274,6 +277,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
OpenGroupManager.INSTANCE.startPolling(); OpenGroupManager.INSTANCE.startPolling();
}); });
// fetch last version data
versionUtil.startTimedVersionCheck();
} }
@Override @Override
@ -286,12 +292,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
poller.stopIfNeeded(); poller.stopIfNeeded();
} }
ClosedGroupPollerV2.getShared().stopAll(); ClosedGroupPollerV2.getShared().stopAll();
versionUtil.stopTimedVersionCheck();
} }
@Override @Override
public void onTerminate() { public void onTerminate() {
stopKovenant(); // Loki stopKovenant(); // Loki
OpenGroupManager.INSTANCE.stopPolling(); OpenGroupManager.INSTANCE.stopPolling();
versionUtil.clear();
super.onTerminate(); super.onTerminate();
} }

View File

@ -18,7 +18,7 @@ fun showMuteDialog(
private enum class Option(@StringRes val stringRes: Int, val getTime: () -> Long) { 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)), 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)),
ONE_DAY(R.string.arrays__mute_for_one_day, duration = TimeUnit.DAYS.toMillis(1)), 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)), SEVEN_DAYS(R.string.arrays__mute_for_seven_days, duration = TimeUnit.DAYS.toMillis(7)),
FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE }); FOREVER(R.string.arrays__mute_forever, getTime = { Long.MAX_VALUE });

View File

@ -22,7 +22,7 @@ public class MemoryFileUtil {
int fd = field.getInt(fileDescriptor); int fd = field.getInt(fileDescriptor);
return ParcelFileDescriptor.adoptFd(fd); return ParcelFileDescriptor.fromFd(fd);
} catch (IllegalAccessException e) { } catch (IllegalAccessException e) {
throw new IOException(e); throw new IOException(e);
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {

View File

@ -0,0 +1,68 @@
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 org.session.libsignal.utilities.Log
import java.util.concurrent.TimeUnit
class VersionUtil(
private val prefs: TextSecurePreferences
) {
private val TAG: String = VersionUtil::class.java.simpleName
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 {
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
job?.cancel()
job = scope.launch {
try {
// perform the version check
val clientVersion = FileServerApi.getClientVersion()
Log.i(TAG, "Fetched version data: $clientVersion")
prefs.setLastVersionCheck()
} catch (e: Exception) {
// we can silently ignore the error
Log.e(TAG, "Error fetching version data: $e")
}
}
}
}

View File

@ -30,6 +30,7 @@ set(SOURCES
config_base.cpp config_base.cpp
contacts.cpp contacts.cpp
conversation.cpp conversation.cpp
blinded_key.cpp
util.cpp) util.cpp)
add_library( # Sets the name of the library. add_library( # Sets the name of the library.

View File

@ -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);
});
}

View File

@ -0,0 +1,15 @@
package network.loki.messenger.libsession_util.util
object BlindKeyAPI {
private val loadLibrary by lazy {
System.loadLibrary("session_util")
}
init {
// Ensure the library is loaded at initialization
loadLibrary
}
external fun blindVersionKeyPair(ed25519SecretKey: ByteArray): KeyPair
external fun blindVersionSign(ed25519SecretKey: ByteArray, timestamp: Long): ByteArray
}

View File

@ -1,18 +1,22 @@
package org.session.libsession.messaging.file_server 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.Promise
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.utilities.await
import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import java.util.concurrent.TimeUnit
object FileServerApi { object FileServerApi {
@ -23,6 +27,7 @@ object FileServerApi {
sealed class Error(message: String) : Exception(message) { sealed class Error(message: String) : Exception(message) {
object ParsingFailed : Error("Invalid response.") object ParsingFailed : Error("Invalid response.")
object InvalidURL : Error("Invalid URL.") object InvalidURL : Error("Invalid URL.")
object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
} }
data class Request( data class Request(
@ -105,4 +110,53 @@ object FileServerApi {
val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file") val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file")
return send(request) 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
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
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
)
}
}
} }

View File

@ -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.
)

View File

@ -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)
fail(cont::resumeWithException)
}
}

View File

@ -13,6 +13,7 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import org.session.libsession.R 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.AUTOPLAY_AUDIO_MESSAGES
import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED 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_DARK
@ -20,6 +21,7 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_
import org.session.libsession.utilities.TextSecurePreferences.Companion.FOLLOW_SYSTEM_SETTINGS 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.HIDE_PASSWORD
import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VACUUM_TIME 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.LEGACY_PREF_KEY_SELECTED_UI_MODE
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_DARK import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_DARK
import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_LIGHT import org.session.libsession.utilities.TextSecurePreferences.Companion.OCEAN_LIGHT
@ -186,6 +188,8 @@ interface TextSecurePreferences {
fun clearAll() fun clearAll()
fun getHidePassword(): Boolean fun getHidePassword(): Boolean
fun setHidePassword(value: Boolean) fun setHidePassword(value: Boolean)
fun getLastVersionCheck(): Long
fun setLastVersionCheck()
companion object { companion object {
val TAG = TextSecurePreferences::class.simpleName val TAG = TextSecurePreferences::class.simpleName
@ -272,6 +276,7 @@ interface TextSecurePreferences {
const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio" const val AUTOPLAY_AUDIO_MESSAGES = "pref_autoplay_audio"
const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated" const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated"
const val SELECTED_ACCENT_COLOR = "selected_accent_color" 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_RECEIVED_LEGACY_CONFIG = "has_received_legacy_config"
const val HAS_FORCED_NEW_CONFIG = "has_forced_new_config" const val HAS_FORCED_NEW_CONFIG = "has_forced_new_config"
@ -1541,6 +1546,14 @@ class AppTextSecurePreferences @Inject constructor(
setLongPreference(LAST_VACUUM_TIME, System.currentTimeMillis()) 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 { override fun setShownCallNotification(): Boolean {
val previousValue = getBooleanPreference(SHOWN_CALL_NOTIFICATION, false) val previousValue = getBooleanPreference(SHOWN_CALL_NOTIFICATION, false)
if (previousValue) return false if (previousValue) return false