From 4b7f5d3cb517a3c506bae1688e440cb928c0bec8 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 19 Jul 2024 16:42:51 +1000 Subject: [PATCH 01/45] 1.19.0 - Bumping release version and code --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4cd08255d6..c526c02d1c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ configurations.all { exclude module: "commons-logging" } -def canonicalVersionCode = 374 -def canonicalVersionName = "1.18.5" +def canonicalVersionCode = 376 +def canonicalVersionName = "1.19.0" def postFixSize = 10 def abiPostFix = ['armeabi-v7a' : 1, From e813756fb3a81cd312ba5f4a33e77163b9092f55 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 25 Jul 2024 16:26:02 +1000 Subject: [PATCH 02/45] Adding the long press fix in 1.19.0 --- .../securesms/conversation/v2/ConversationActivityV2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 62ea2e52d4..1e107df6ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -37,7 +37,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.core.text.set import androidx.core.text.toSpannable -import androidx.core.view.drawToBitmap import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.DialogFragment @@ -173,6 +172,7 @@ import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.NetworkUtils import org.thoughtcrime.securesms.util.SaveAttachmentTask +import org.thoughtcrime.securesms.util.drawToBitmap import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom import org.thoughtcrime.securesms.util.push From 143d4c462a4cb83f7c3da9575a034854dedab8cc Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 26 Jul 2024 09:47:57 +0930 Subject: [PATCH 03/45] Show Account Created empty view only for newAccounts --- .../securesms/home/HomeActivity.kt | 20 +++++++++---------- .../onboarding/loading/LoadingActivity.kt | 2 +- .../MessageNotificationsActivity.kt | 2 +- .../pickname/PickDisplayNameActivity.kt | 2 +- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index ee82a708c4..ff520f3e81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -80,16 +80,14 @@ import org.thoughtcrime.securesms.util.start import java.io.IOException import javax.inject.Inject +private const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT" +private const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING" + @AndroidEntryPoint class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, GlobalSearchInputLayout.GlobalSearchInputLayoutListener { - companion object { - const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT" - const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING" - } - private lateinit var binding: ActivityHomeBinding private lateinit var glide: GlideRequests @@ -137,7 +135,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } - private val isNewAccount: Boolean get() = intent.getBooleanExtra(FROM_ONBOARDING, false) + private val isFromOnboarding: Boolean get() = intent.getBooleanExtra(FROM_ONBOARDING, false) + private val isNewAccount: Boolean get() = intent.getBooleanExtra(NEW_ACCOUNT, false) // region Lifecycle override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { @@ -266,8 +265,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } EventBus.getDefault().register(this@HomeActivity) - if (intent.hasExtra(FROM_ONBOARDING) - && intent.getBooleanExtra(FROM_ONBOARDING, false)) { + if (isFromOnboarding) { if (Build.VERSION.SDK_INT >= 33 && (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) { Permissions.with(this) @@ -639,10 +637,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } -fun Context.startHomeActivity(isNewAccount: Boolean) { +fun Context.startHomeActivity(isFromOnboarding: Boolean, isNewAccount: Boolean) { Intent(this, HomeActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - putExtra(HomeActivity.NEW_ACCOUNT, true) - putExtra(HomeActivity.FROM_ONBOARDING, true) + putExtra(NEW_ACCOUNT, isNewAccount) + putExtra(FROM_ONBOARDING, isFromOnboarding) }.also(::startActivity) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt index 9c8a1869d4..abf0471598 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/loading/LoadingActivity.kt @@ -32,7 +32,7 @@ class LoadingActivity: BaseActionBarActivity() { when { loadFailed -> startPickDisplayNameActivity(loadFailed = true) - else -> startHomeActivity(isNewAccount = false) + else -> startHomeActivity(isNewAccount = false, isFromOnboarding = true) } finish() diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt index 42cf49bce8..9ea4fd2bd6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt @@ -49,7 +49,7 @@ class MessageNotificationsActivity : BaseActionBarActivity() { viewModel.events.collect { when (it) { Event.Loading -> start() - Event.OnboardingComplete -> startHomeActivity(isNewAccount = true) + Event.OnboardingComplete -> startHomeActivity(isNewAccount = true, isFromOnboarding = true) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt index 97aacf82e6..8ade5b4e8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt @@ -45,7 +45,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() { viewModel.events.collect { when (it) { is Event.CreateAccount -> startMessageNotificationsActivity(it.profileName) - Event.LoadAccountComplete -> startHomeActivity(isNewAccount = false) + Event.LoadAccountComplete -> startHomeActivity(isNewAccount = false, isFromOnboarding = true) } } } From af302d4d43fe83a23300c6b335f0458c9f50bca3 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 26 Jul 2024 10:13:51 +0930 Subject: [PATCH 04/45] Change conversations heading in search --- .../main/java/org/thoughtcrime/securesms/home/HomeActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index ee82a708c4..7cff76beb6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -253,7 +253,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } else -> buildList { result.contactAndGroupList.takeUnless { it.isEmpty() }?.let { - add(GlobalSearchAdapter.Model.Header(R.string.contacts)) + add(GlobalSearchAdapter.Model.Header(R.string.conversations)) addAll(it) } result.messageResults.takeUnless { it.isEmpty() }?.let { From 456f8d0b3a574e370686db8c9343f36af59a5f99 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 26 Jul 2024 15:49:41 +1000 Subject: [PATCH 05/45] Handling keyboard inset for Android sdk < 30 --- .../start/NewConversationFragment.kt | 6 +- .../start/newmessage/NewMessage.kt | 175 ++++++++++++------ 2 files changed, 126 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt index 1ffc65f592..8dffb1fd9b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/NewConversationFragment.kt @@ -27,7 +27,11 @@ import org.thoughtcrime.securesms.groups.JoinCommunityFragment @AndroidEntryPoint class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate { - private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * 0.94).toInt() } + companion object{ + const val PEEK_RATIO = 0.94f + } + + private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * PEEK_RATIO).toInt() } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt index 97740b2a26..c3c33323cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt @@ -1,10 +1,13 @@ package org.thoughtcrime.securesms.conversation.start.newmessage -import androidx.compose.animation.AnimatedVisibility +import android.graphics.Rect +import android.os.Build +import android.view.ViewTreeObserver import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -15,23 +18,31 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import network.loki.messenger.R -import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton +import org.thoughtcrime.securesms.conversation.start.StartConversationFragment.Companion.PEEK_RATIO import org.thoughtcrime.securesms.ui.LoadingArcOr -import org.thoughtcrime.securesms.ui.theme.LocalDimensions -import org.thoughtcrime.securesms.ui.theme.PreviewTheme -import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider -import org.thoughtcrime.securesms.ui.theme.ThemeColors -import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.components.AppBar import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode @@ -39,7 +50,13 @@ import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.contentDescription +import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors +import kotlin.math.max private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan) @@ -76,63 +93,113 @@ private fun EnterAccountId( callbacks: Callbacks, onHelp: () -> Unit = {} ) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .imePadding() - ) { - Column( - modifier = Modifier.padding(vertical = LocalDimensions.current.spacing), - horizontalAlignment = Alignment.CenterHorizontally, + // the scaffold is required to provide the contentPadding. That contentPadding is needed + // to properly handle the ime padding. + Scaffold() { contentPadding -> + // we need this extra surface to handle nested scrolling properly, + // because this scrollable component is inside a bottomSheet dialog which is itself scrollable + Surface( + modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()), + color = LocalColors.current.backgroundSecondary ) { - SessionOutlinedTextField( - text = state.newMessageIdOrOns, - modifier = Modifier - .padding(horizontal = LocalDimensions.current.spacing), - contentDescription = "Session id input box", - placeholder = stringResource(R.string.accountIdOrOnsEnter), - onChange = callbacks::onChange, - onContinue = callbacks::onContinue, - error = state.error?.string(), - isTextErrorColor = state.isTextErrorColor - ) - Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) + var accountModifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) - BorderlessButtonWithIcon( - text = stringResource(R.string.messageNewDescription), - modifier = Modifier - .contentDescription(R.string.AccessibilityId_help_desk_link) - .padding(horizontal = LocalDimensions.current.mediumSpacing) - .fillMaxWidth(), - style = LocalType.current.small, - color = LocalColors.current.textSecondary, - iconRes = R.drawable.ic_circle_question_mark, - onClick = onHelp - ) - } + // There is a known issue with the ime padding on android versions below 30 + // So we these older versions we need to resort to some manual padding based on the visible height + // when the keyboard is up + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + val keyboardHeight by keyboardHeight() + accountModifier = accountModifier.padding(bottom = keyboardHeight) + } else { + accountModifier = accountModifier + .consumeWindowInsets(contentPadding) + .imePadding() + } - Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) - Spacer(Modifier.weight(2f)) + Column( + modifier = accountModifier + ) { + Column( + modifier = Modifier.padding(vertical = LocalDimensions.current.spacing), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SessionOutlinedTextField( + text = state.newMessageIdOrOns, + modifier = Modifier + .padding(horizontal = LocalDimensions.current.spacing), + contentDescription = "Session id input box", + placeholder = stringResource(R.string.accountIdOrOnsEnter), + onChange = callbacks::onChange, + onContinue = callbacks::onContinue, + error = state.error?.string(), + isTextErrorColor = state.isTextErrorColor + ) - PrimaryOutlineButton( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(horizontal = LocalDimensions.current.xlargeSpacing) - .padding(bottom = LocalDimensions.current.smallSpacing) - .fillMaxWidth() - .contentDescription(R.string.next), - enabled = state.isNextButtonEnabled, - onClick = callbacks::onContinue - ) { - LoadingArcOr(state.loading) { - Text(stringResource(R.string.next)) + Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) + + BorderlessButtonWithIcon( + text = stringResource(R.string.messageNewDescription), + modifier = Modifier + .contentDescription(R.string.AccessibilityId_help_desk_link) + .padding(horizontal = LocalDimensions.current.mediumSpacing) + .fillMaxWidth(), + style = LocalType.current.small, + color = LocalColors.current.textSecondary, + iconRes = R.drawable.ic_circle_question_mark, + onClick = onHelp + ) + } + + Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) + Spacer(Modifier.weight(2f)) + + PrimaryOutlineButton( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(horizontal = LocalDimensions.current.xlargeSpacing) + .padding(bottom = LocalDimensions.current.smallSpacing) + .fillMaxWidth() + .contentDescription(R.string.next), + enabled = state.isNextButtonEnabled, + onClick = callbacks::onContinue + ) { + LoadingArcOr(state.loading) { + Text(stringResource(R.string.next)) + } + } } } } } +@Composable +fun keyboardHeight(): MutableState { + val view = LocalView.current + var keyboardHeight = remember { mutableStateOf(0.dp) } + val density = LocalDensity.current + + DisposableEffect(view) { + val listener = ViewTreeObserver.OnGlobalLayoutListener { + val rect = Rect() + view.getWindowVisibleDisplayFrame(rect) + val screenHeight = view.rootView.height * PEEK_RATIO + val keypadHeightPx = max( screenHeight - rect.bottom, 0f) + + keyboardHeight.value = with(density) { keypadHeightPx.toDp() } + } + + view.viewTreeObserver.addOnGlobalLayoutListener(listener) + onDispose { + view.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + } + + return keyboardHeight +} + @Preview @Composable private fun PreviewNewMessage( From 55ec4e154939184441c1b5afdcd41780aa3459e9 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 26 Jul 2024 16:23:33 +1000 Subject: [PATCH 06/45] Update app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com> --- .../securesms/conversation/start/newmessage/NewMessage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt index c3c33323cf..f0a6e21b4c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessage.kt @@ -108,7 +108,7 @@ private fun EnterAccountId( .verticalScroll(rememberScrollState()) // There is a known issue with the ime padding on android versions below 30 - // So we these older versions we need to resort to some manual padding based on the visible height + /// So on these older versions we need to resort to some manual padding based on the visible height // when the keyboard is up if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { val keyboardHeight by keyboardHeight() From b4f13bbe82fc246bee45457302e4950a49561891 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Fri, 26 Jul 2024 17:21:06 +1000 Subject: [PATCH 07/45] Trimming the accound ID when validating it --- .../conversation/start/newmessage/NewMessageViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt index 65c8dd539a..6ed8a08233 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/start/newmessage/NewMessageViewModel.kt @@ -46,7 +46,7 @@ internal class NewMessageViewModel @Inject constructor( } override fun onContinue() { - val idOrONS = state.value.newMessageIdOrOns + val idOrONS = state.value.newMessageIdOrOns.trim() if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) { onUnvalidatedPublicKey(publicKey = idOrONS) From 80d08a5fb242c78c96724b2846525b0517961d2a Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 26 Jul 2024 21:43:10 +0930 Subject: [PATCH 08/45] Fix follow light --- .../appearance/AppearanceSettingsViewModel.kt | 9 +++-- .../securesms/ui/theme/ThemeColorSet.kt | 4 +- .../ui/theme/ThemeFromPreferences.kt | 38 +++++++------------ .../thoughtcrime/securesms/ui/theme/Themes.kt | 20 +++++----- 4 files changed, 34 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt index afe33400ff..fcc1ffb4bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt @@ -6,7 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.session.libsession.utilities.TextSecurePreferences -import org.thoughtcrime.securesms.ui.theme.selectedTheme +import org.thoughtcrime.securesms.ui.theme.selectedColorSet import org.thoughtcrime.securesms.util.ThemeState import org.thoughtcrime.securesms.util.themeState import javax.inject.Inject @@ -21,6 +21,9 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec prefs.setAccentColorStyle(newAccentColorStyle) // update UI state _uiState.value = prefs.themeState() + + // force compose to refresh its style reference + selectedColorSet = null } fun setNewStyle(newThemeStyle: String) { @@ -29,7 +32,7 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec _uiState.value = prefs.themeState() // force compose to refresh its style reference - selectedTheme = null + selectedColorSet = null } fun setNewFollowSystemSettings(followSystemSettings: Boolean) { @@ -37,7 +40,7 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec _uiState.value = prefs.themeState() // force compose to refresh its style reference - selectedTheme = null + selectedColorSet = null } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt index 3ff4d55b61..59ea383063 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt @@ -7,4 +7,6 @@ package org.thoughtcrime.securesms.ui.theme data class ThemeColorSet( val light: ThemeColors, val dark: ThemeColors -) \ No newline at end of file +) { + fun get(isDark: Boolean): ThemeColors = if (isDark) dark else light +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt index 8988e84ca7..130c370e26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt @@ -17,38 +17,28 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.YELLOW_A * Some behaviour is hardcoded to cater for legacy usage of people with themes already set * But future themes will be picked and set directly from the "Appearance" screen */ -@Composable -fun TextSecurePreferences.getComposeTheme(): ThemeColors { +fun TextSecurePreferences.getColorSet(): ThemeColorSet { val selectedTheme = getThemeStyle() // get the chosen primary color from the preferences val selectedPrimary = primaryColor() - // create a theme set with the appropriate primary - val colorSet = when(selectedTheme){ - TextSecurePreferences.OCEAN_DARK, - TextSecurePreferences.OCEAN_LIGHT -> ThemeColorSet( - light = OceanLight(selectedPrimary), - dark = OceanDark(selectedPrimary) - ) + val createLight = if ("ocean" in selectedTheme) ::OceanLight else ::ClassicLight + val createDark = if ("ocean" in selectedTheme) ::OceanDark else ::ClassicDark - else -> ThemeColorSet( - light = ClassicLight(selectedPrimary), - dark = ClassicDark(selectedPrimary) + val followSystemSettings = getFollowSystemSettings() + + return if (followSystemSettings) ThemeColorSet( + light = createLight(selectedPrimary), + dark = createDark(selectedPrimary) + ) else { + val both = if ("light" in selectedTheme) createLight(selectedPrimary) else createDark(selectedPrimary) + + ThemeColorSet( + light = both, + dark = both ) } - - // deliver the right set from the light/dark mode chosen - val theme = when{ - getFollowSystemSettings() -> if(isSystemInDarkTheme()) colorSet.dark else colorSet.light - - selectedTheme == TextSecurePreferences.CLASSIC_LIGHT || - selectedTheme == TextSecurePreferences.OCEAN_LIGHT -> colorSet.light - - else -> colorSet.dark - } - - return theme } fun TextSecurePreferences.primaryColor(): Color = when(getSelectedAccentColor()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index 87e91e0ab0..d4b05572f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.ui.theme import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.LocalTextSelectionColors @@ -15,12 +16,13 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import org.session.libsession.utilities.AppTextSecurePreferences +import org.session.libsession.utilities.TextSecurePreferences // Globally accessible composition local objects val LocalColors = compositionLocalOf { ClassicDark() } val LocalType = compositionLocalOf { sessionTypography } -var selectedTheme: ThemeColors? = null +var selectedColorSet: ThemeColorSet? = null /** * Apply a Material2 compose theme based on user selections in SharedPreferences. @@ -29,15 +31,15 @@ var selectedTheme: ThemeColors? = null fun SessionMaterialTheme( content: @Composable () -> Unit ) { - // set the theme data if it hasn't been done yet - if(selectedTheme == null) { - // Some values can be set from the preferences, and if not should fallback to a default value - val context = LocalContext.current - val preferences = AppTextSecurePreferences(context) - selectedTheme = preferences.getComposeTheme() - } + val context = LocalContext.current + val preferences = AppTextSecurePreferences(context) - SessionMaterialTheme(colors = selectedTheme ?: ClassicDark()) { content() } + val selectedColorSet = selectedColorSet ?: preferences.getColorSet().also { selectedColorSet = it } + + SessionMaterialTheme( + colors = selectedColorSet.get(isSystemInDarkTheme()), + content = content + ) } /** From 90f6fee579fdd6dd0cfea6dba1ec52fb5eefb945 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 26 Jul 2024 21:49:55 +0930 Subject: [PATCH 09/45] Change ThemeColorSet naming --- .../org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt | 6 +++--- .../securesms/ui/theme/ThemeFromPreferences.kt | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt index 59ea383063..a4b15e456b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt @@ -5,8 +5,8 @@ package org.thoughtcrime.securesms.ui.theme * light theme, and [dark] representing the [ThemeColors] to use when the system is in a dark theme. */ data class ThemeColorSet( - val light: ThemeColors, - val dark: ThemeColors + val colorsWhenSystemInLight: ThemeColors, + val colorsWhenSystemInDark: ThemeColors ) { - fun get(isDark: Boolean): ThemeColors = if (isDark) dark else light + fun get(isDark: Boolean): ThemeColors = if (isDark) colorsWhenSystemInDark else colorsWhenSystemInLight } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt index 130c370e26..d3e0927635 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt @@ -29,14 +29,14 @@ fun TextSecurePreferences.getColorSet(): ThemeColorSet { val followSystemSettings = getFollowSystemSettings() return if (followSystemSettings) ThemeColorSet( - light = createLight(selectedPrimary), - dark = createDark(selectedPrimary) + colorsWhenSystemInLight = createLight(selectedPrimary), + colorsWhenSystemInDark = createDark(selectedPrimary) ) else { val both = if ("light" in selectedTheme) createLight(selectedPrimary) else createDark(selectedPrimary) ThemeColorSet( - light = both, - dark = both + colorsWhenSystemInLight = both, + colorsWhenSystemInDark = both ) } } From 49ecdfd1106c96181f01d5fea39bfa1eee5bc05d Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Sat, 27 Jul 2024 16:12:03 +0930 Subject: [PATCH 10/45] Fix app exit after Lock Screen --- app/src/main/AndroidManifest.xml | 2 +- .../main/java/org/thoughtcrime/securesms/home/HomeActivity.kt | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7e04ad44ca..d70c9e080e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -125,7 +125,7 @@ Date: Mon, 29 Jul 2024 13:00:59 +0930 Subject: [PATCH 11/45] Fix dialog not shown on seed send attempt --- .../conversation/v2/ConversationActivityV2.kt | 13 +++++++---- .../conversation/v2/dialogs/SendSeedDialog.kt | 23 ------------------- 2 files changed, 9 insertions(+), 27 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 1e107df6ae..1e6eb2489a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -164,7 +164,6 @@ import org.thoughtcrime.securesms.mms.VideoSlide import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment -import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities @@ -177,7 +176,6 @@ import org.thoughtcrime.securesms.util.isScrolledToBottom import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.show -import org.thoughtcrime.securesms.util.start import org.thoughtcrime.securesms.util.toPx import java.lang.ref.WeakReference import java.util.Locale @@ -1589,8 +1587,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val text = getMessageBody() val userPublicKey = textSecurePreferences.getLocalNumber() val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey) - if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) { - start() + if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) { + showSessionDialog { + title(R.string.dialog_send_seed_title) + text(R.string.dialog_send_seed_explanation) + button(R.string.dialog_send_seed_send_button_title) { sendTextOnlyMessage(true) } + cancelButton() + } + + return null } // Create the message val message = VisibleMessage().applyExpiryMode(viewModel.threadId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt deleted file mode 100644 index 6abb0814d6..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/SendSeedDialog.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.thoughtcrime.securesms.conversation.v2.dialogs - -import android.app.Dialog -import android.os.Bundle -import androidx.fragment.app.DialogFragment -import network.loki.messenger.R -import org.thoughtcrime.securesms.createSessionDialog - -/** Shown if the user is about to send their recovery phrase to someone. */ -class SendSeedDialog(private val proceed: (() -> Unit)? = null) : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { - title(R.string.dialog_send_seed_title) - text(R.string.dialog_send_seed_explanation) - button(R.string.dialog_send_seed_send_button_title) { send() } - cancelButton() - } - - private fun send() { - proceed?.invoke() - dismiss() - } -} From 492d5217d0e137f9d711c163339d5b954b1399fd Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Mon, 29 Jul 2024 14:49:37 +0930 Subject: [PATCH 12/45] Add colors lambda --- .../appearance/AppearanceSettingsViewModel.kt | 17 ++++++------ .../securesms/ui/theme/ThemeColorSet.kt | 12 --------- .../ui/theme/ThemeFromPreferences.kt | 26 +++++++++---------- .../thoughtcrime/securesms/ui/theme/Themes.kt | 7 +++-- 4 files changed, 24 insertions(+), 38 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt index fcc1ffb4bc..8a125896cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt @@ -6,7 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.session.libsession.utilities.TextSecurePreferences -import org.thoughtcrime.securesms.ui.theme.selectedColorSet +import org.thoughtcrime.securesms.ui.theme.cachedColors import org.thoughtcrime.securesms.util.ThemeState import org.thoughtcrime.securesms.util.themeState import javax.inject.Inject @@ -22,8 +22,8 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec // update UI state _uiState.value = prefs.themeState() - // force compose to refresh its style reference - selectedColorSet = null + // invalidate compose theme colors + cachedColors = null } fun setNewStyle(newThemeStyle: String) { @@ -31,16 +31,15 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec // update UI state _uiState.value = prefs.themeState() - // force compose to refresh its style reference - selectedColorSet = null + // invalidate compose theme colors + cachedColors = null } fun setNewFollowSystemSettings(followSystemSettings: Boolean) { prefs.setFollowSystemSettings(followSystemSettings) _uiState.value = prefs.themeState() - // force compose to refresh its style reference - selectedColorSet = null + // invalidate compose theme colors + cachedColors = null } - -} \ No newline at end of file +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt deleted file mode 100644 index a4b15e456b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorSet.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.thoughtcrime.securesms.ui.theme - -/** - * This class holds two instances of [ThemeColors], [light] representing the [ThemeColors] to use when the system is in a - * light theme, and [dark] representing the [ThemeColors] to use when the system is in a dark theme. - */ -data class ThemeColorSet( - val colorsWhenSystemInLight: ThemeColors, - val colorsWhenSystemInDark: ThemeColors -) { - fun get(isDark: Boolean): ThemeColors = if (isDark) colorsWhenSystemInDark else colorsWhenSystemInLight -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt index d3e0927635..c930dd4b2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt @@ -17,27 +17,27 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.YELLOW_A * Some behaviour is hardcoded to cater for legacy usage of people with themes already set * But future themes will be picked and set directly from the "Appearance" screen */ -fun TextSecurePreferences.getColorSet(): ThemeColorSet { +val TextSecurePreferences.colors: @Composable () -> ThemeColors get() { val selectedTheme = getThemeStyle() // get the chosen primary color from the preferences val selectedPrimary = primaryColor() - val createLight = if ("ocean" in selectedTheme) ::OceanLight else ::ClassicLight - val createDark = if ("ocean" in selectedTheme) ::OceanDark else ::ClassicDark + val isOcean = "ocean" in selectedTheme - val followSystemSettings = getFollowSystemSettings() + val createLight = if (isOcean) ::OceanLight else ::ClassicLight + val createDark = if (isOcean) ::OceanDark else ::ClassicDark - return if (followSystemSettings) ThemeColorSet( - colorsWhenSystemInLight = createLight(selectedPrimary), - colorsWhenSystemInDark = createDark(selectedPrimary) - ) else { - val both = if ("light" in selectedTheme) createLight(selectedPrimary) else createDark(selectedPrimary) + // create the light and dark themes outside the lambda to avoid creating them every time + // [SessionMaterialTheme] is called. Creating both when we don't followSystemSettings is but a + // minor inefficiency that increases readability. + val light = createLight(selectedPrimary) + val dark = createDark(selectedPrimary) - ThemeColorSet( - colorsWhenSystemInLight = both, - colorsWhenSystemInDark = both - ) + return when { + getFollowSystemSettings() -> { { if (isSystemInDarkTheme()) dark else light } } + "light" in selectedTheme -> { { light } } + else -> { { dark } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index d4b05572f6..ffce1a8e2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -16,13 +16,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import org.session.libsession.utilities.AppTextSecurePreferences -import org.session.libsession.utilities.TextSecurePreferences // Globally accessible composition local objects val LocalColors = compositionLocalOf { ClassicDark() } val LocalType = compositionLocalOf { sessionTypography } -var selectedColorSet: ThemeColorSet? = null +var cachedColors: (@Composable () -> ThemeColors)? = null /** * Apply a Material2 compose theme based on user selections in SharedPreferences. @@ -34,10 +33,10 @@ fun SessionMaterialTheme( val context = LocalContext.current val preferences = AppTextSecurePreferences(context) - val selectedColorSet = selectedColorSet ?: preferences.getColorSet().also { selectedColorSet = it } + val cachedColors = cachedColors ?: preferences.colors.also { cachedColors = it } SessionMaterialTheme( - colors = selectedColorSet.get(isSystemInDarkTheme()), + colors = cachedColors(), content = content ) } From 5bd2724decea7bc3ba9e7c2a800154165318733a Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 29 Jul 2024 16:10:15 +1000 Subject: [PATCH 13/45] Latest libsession --- libsession-util/libsession-util | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsession-util/libsession-util b/libsession-util/libsession-util index 20c06674d8..0193c36e0d 160000 --- a/libsession-util/libsession-util +++ b/libsession-util/libsession-util @@ -1 +1 @@ -Subproject commit 20c06674d85369c2d12261582dd36a9f21504233 +Subproject commit 0193c36e0dad461385d6407a00f33b7314e6d740 From 25e7c7ec6111a100d09bd55279454b9ae8525edf Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Mon, 29 Jul 2024 16:32:26 +0930 Subject: [PATCH 14/45] Simplify ThemeFromPreferences by removing lambdas --- .../appearance/AppearanceSettingsViewModel.kt | 14 +++++++------ .../ui/theme/MaybeFollowSystemColors.kt | 18 +++++++++++++++++ .../ui/theme/ThemeFromPreferences.kt | 20 +++++++------------ .../thoughtcrime/securesms/ui/theme/Themes.kt | 12 +++++------ 4 files changed, 38 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt index 8a125896cf..c662a7a772 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt @@ -17,13 +17,17 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec private val _uiState = MutableStateFlow(prefs.themeState()) val uiState: StateFlow = _uiState + fun invalidateComposeThemeColors() { + // invalidate compose theme colors + cachedColors = null + } + fun setNewAccent(@StyleRes newAccentColorStyle: Int) { prefs.setAccentColorStyle(newAccentColorStyle) // update UI state _uiState.value = prefs.themeState() - // invalidate compose theme colors - cachedColors = null + invalidateComposeThemeColors() } fun setNewStyle(newThemeStyle: String) { @@ -31,15 +35,13 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec // update UI state _uiState.value = prefs.themeState() - // invalidate compose theme colors - cachedColors = null + invalidateComposeThemeColors() } fun setNewFollowSystemSettings(followSystemSettings: Boolean) { prefs.setFollowSystemSettings(followSystemSettings) _uiState.value = prefs.themeState() - // invalidate compose theme colors - cachedColors = null + invalidateComposeThemeColors() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt new file mode 100644 index 0000000000..fa0353a1e6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt @@ -0,0 +1,18 @@ +package org.thoughtcrime.securesms.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable + +fun interface MaybeFollowSystemColors { + @Composable + fun get(): ThemeColors +} + +fun FollowSystemColors(light: ThemeColors, dark: ThemeColors) = MaybeFollowSystemColors { + when { + isSystemInDarkTheme() -> dark + else -> light + } +} + +fun IgnoreSystemColors(colors: ThemeColors) = MaybeFollowSystemColors { colors } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt index c930dd4b2b..3b111526c7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt @@ -17,7 +17,7 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.YELLOW_A * Some behaviour is hardcoded to cater for legacy usage of people with themes already set * But future themes will be picked and set directly from the "Appearance" screen */ -val TextSecurePreferences.colors: @Composable () -> ThemeColors get() { +val TextSecurePreferences.colors: MaybeFollowSystemColors get() { val selectedTheme = getThemeStyle() // get the chosen primary color from the preferences @@ -28,16 +28,13 @@ val TextSecurePreferences.colors: @Composable () -> ThemeColors get() { val createLight = if (isOcean) ::OceanLight else ::ClassicLight val createDark = if (isOcean) ::OceanDark else ::ClassicDark - // create the light and dark themes outside the lambda to avoid creating them every time - // [SessionMaterialTheme] is called. Creating both when we don't followSystemSettings is but a - // minor inefficiency that increases readability. - val light = createLight(selectedPrimary) - val dark = createDark(selectedPrimary) - return when { - getFollowSystemSettings() -> { { if (isSystemInDarkTheme()) dark else light } } - "light" in selectedTheme -> { { light } } - else -> { { dark } } + getFollowSystemSettings() -> FollowSystemColors( + light = createLight(selectedPrimary), + dark = createDark(selectedPrimary) + ) + "light" in selectedTheme -> IgnoreSystemColors(createLight(selectedPrimary)) + else -> IgnoreSystemColors(createDark(selectedPrimary)) } } @@ -50,6 +47,3 @@ fun TextSecurePreferences.primaryColor(): Color = when(getSelectedAccentColor()) YELLOW_ACCENT -> primaryYellow else -> primaryGreen } - - - diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index ffce1a8e2b..d1105ccbc1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.ui.theme import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.LocalTextSelectionColors @@ -21,7 +20,7 @@ import org.session.libsession.utilities.AppTextSecurePreferences val LocalColors = compositionLocalOf { ClassicDark() } val LocalType = compositionLocalOf { sessionTypography } -var cachedColors: (@Composable () -> ThemeColors)? = null +var cachedColors: MaybeFollowSystemColors? = null /** * Apply a Material2 compose theme based on user selections in SharedPreferences. @@ -33,10 +32,10 @@ fun SessionMaterialTheme( val context = LocalContext.current val preferences = AppTextSecurePreferences(context) - val cachedColors = cachedColors ?: preferences.colors.also { cachedColors = it } + val jjcachedColors = cachedColors ?: preferences.colors.also { cachedColors = it } SessionMaterialTheme( - colors = cachedColors(), + colors = cachedColors.get(), content = content ) } @@ -59,9 +58,8 @@ fun SessionMaterialTheme( LocalType provides sessionTypography, LocalContentColor provides colors.text, LocalTextSelectionColors provides colors.textSelectionColors, - ) { - content() - } + content = content + ) } } From 7bb1a3a513035b8acb012a9717e611f46674fd08 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Mon, 29 Jul 2024 16:36:17 +0930 Subject: [PATCH 15/45] Suppress compose name warning --- .../thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt | 2 ++ app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt index fa0353a1e6..6bb0d3c2ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt @@ -8,6 +8,7 @@ fun interface MaybeFollowSystemColors { fun get(): ThemeColors } +@Suppress("FunctionName") fun FollowSystemColors(light: ThemeColors, dark: ThemeColors) = MaybeFollowSystemColors { when { isSystemInDarkTheme() -> dark @@ -15,4 +16,5 @@ fun FollowSystemColors(light: ThemeColors, dark: ThemeColors) = MaybeFollowSyste } } +@Suppress("FunctionName") fun IgnoreSystemColors(colors: ThemeColors) = MaybeFollowSystemColors { colors } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index d1105ccbc1..ec19b3b100 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -32,7 +32,7 @@ fun SessionMaterialTheme( val context = LocalContext.current val preferences = AppTextSecurePreferences(context) - val jjcachedColors = cachedColors ?: preferences.colors.also { cachedColors = it } + val cachedColors = cachedColors ?: preferences.colors.also { cachedColors = it } SessionMaterialTheme( colors = cachedColors.get(), From dba0ca910e40631019f33f434794aa2581c746aa Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 29 Jul 2024 17:20:44 +1000 Subject: [PATCH 16/45] JNI bridging for the new version blinded key api --- libsession-util/src/main/cpp/CMakeLists.txt | 1 + libsession-util/src/main/cpp/blinded_key.cpp | 34 +++++++++++++++++++ .../libsession_util/util/BlindKeyAPI.kt | 10 ++++++ 3 files changed, 45 insertions(+) create mode 100644 libsession-util/src/main/cpp/blinded_key.cpp create mode 100644 libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt diff --git a/libsession-util/src/main/cpp/CMakeLists.txt b/libsession-util/src/main/cpp/CMakeLists.txt index 47fee4803c..f65667bcb5 100644 --- a/libsession-util/src/main/cpp/CMakeLists.txt +++ b/libsession-util/src/main/cpp/CMakeLists.txt @@ -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. diff --git a/libsession-util/src/main/cpp/blinded_key.cpp b/libsession-util/src/main/cpp/blinded_key.cpp new file mode 100644 index 0000000000..1ed1cfd554 --- /dev/null +++ b/libsession-util/src/main/cpp/blinded_key.cpp @@ -0,0 +1,34 @@ +#include +#include + +#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(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, "", "([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(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); + }); +} \ No newline at end of file diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt new file mode 100644 index 0000000000..8f57caf071 --- /dev/null +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt @@ -0,0 +1,10 @@ +package network.loki.messenger.libsession_util.util + +object BlindKeyAPI { + init { + System.loadLibrary("session_util") + } + + external fun blindVersionKeyPair(ed25519SecretKey: ByteArray): KeyPair + external fun blindVersionSign(ed25519SecretKey: ByteArray, timestamp: Long): ByteArray +} \ No newline at end of file From d23d8f3b07811c4dcbd6d0451abc63d775654028 Mon Sep 17 00:00:00 2001 From: Fanchao Liu <273191+simophin@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:42:26 +1000 Subject: [PATCH 17/45] Fix double closing on memory file (#1579) Co-authored-by: fanchao --- .../java/org/thoughtcrime/securesms/util/MemoryFileUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MemoryFileUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/MemoryFileUtil.java index 4ee43e1e9c..f193827efd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MemoryFileUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MemoryFileUtil.java @@ -22,7 +22,7 @@ public class MemoryFileUtil { int fd = field.getInt(fileDescriptor); - return ParcelFileDescriptor.adoptFd(fd); + return ParcelFileDescriptor.fromFd(fd); } catch (IllegalAccessException e) { throw new IOException(e); } catch (InvocationTargetException e) { From 447ea85333df87632d90165d7230342a6e819ba5 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Mon, 29 Jul 2024 17:35:55 +0930 Subject: [PATCH 18/45] Improve naming --- .../appearance/AppearanceSettingsViewModel.kt | 4 ++-- ...ybeFollowSystemColors.kt => ThemeColorsProvider.kt} | 6 +++--- .../securesms/ui/theme/ThemeFromPreferences.kt | 10 ++++------ .../java/org/thoughtcrime/securesms/ui/theme/Themes.kt | 4 ++-- 4 files changed, 11 insertions(+), 13 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/ui/theme/{MaybeFollowSystemColors.kt => ThemeColorsProvider.kt} (60%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt index c662a7a772..47cc9f69bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt @@ -6,7 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.session.libsession.utilities.TextSecurePreferences -import org.thoughtcrime.securesms.ui.theme.cachedColors +import org.thoughtcrime.securesms.ui.theme.cachedColorsProvider import org.thoughtcrime.securesms.util.ThemeState import org.thoughtcrime.securesms.util.themeState import javax.inject.Inject @@ -19,7 +19,7 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec fun invalidateComposeThemeColors() { // invalidate compose theme colors - cachedColors = null + cachedColorsProvider = null } fun setNewAccent(@StyleRes newAccentColorStyle: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorsProvider.kt similarity index 60% rename from app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt rename to app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorsProvider.kt index 6bb0d3c2ad..0713254afc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/MaybeFollowSystemColors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeColorsProvider.kt @@ -3,13 +3,13 @@ package org.thoughtcrime.securesms.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable -fun interface MaybeFollowSystemColors { +fun interface ThemeColorsProvider { @Composable fun get(): ThemeColors } @Suppress("FunctionName") -fun FollowSystemColors(light: ThemeColors, dark: ThemeColors) = MaybeFollowSystemColors { +fun FollowSystemThemeColorsProvider(light: ThemeColors, dark: ThemeColors) = ThemeColorsProvider { when { isSystemInDarkTheme() -> dark else -> light @@ -17,4 +17,4 @@ fun FollowSystemColors(light: ThemeColors, dark: ThemeColors) = MaybeFollowSyste } @Suppress("FunctionName") -fun IgnoreSystemColors(colors: ThemeColors) = MaybeFollowSystemColors { colors } +fun ThemeColorsProvider(colors: ThemeColors) = ThemeColorsProvider { colors } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt index 3b111526c7..403235ef79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/ThemeFromPreferences.kt @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.ui.theme -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences.Companion.BLUE_ACCENT @@ -17,7 +15,7 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.YELLOW_A * Some behaviour is hardcoded to cater for legacy usage of people with themes already set * But future themes will be picked and set directly from the "Appearance" screen */ -val TextSecurePreferences.colors: MaybeFollowSystemColors get() { +fun TextSecurePreferences.getColorsProvider(): ThemeColorsProvider { val selectedTheme = getThemeStyle() // get the chosen primary color from the preferences @@ -29,12 +27,12 @@ val TextSecurePreferences.colors: MaybeFollowSystemColors get() { val createDark = if (isOcean) ::OceanDark else ::ClassicDark return when { - getFollowSystemSettings() -> FollowSystemColors( + getFollowSystemSettings() -> FollowSystemThemeColorsProvider( light = createLight(selectedPrimary), dark = createDark(selectedPrimary) ) - "light" in selectedTheme -> IgnoreSystemColors(createLight(selectedPrimary)) - else -> IgnoreSystemColors(createDark(selectedPrimary)) + "light" in selectedTheme -> ThemeColorsProvider(createLight(selectedPrimary)) + else -> ThemeColorsProvider(createDark(selectedPrimary)) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index ec19b3b100..9b83028ec8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -20,7 +20,7 @@ import org.session.libsession.utilities.AppTextSecurePreferences val LocalColors = compositionLocalOf { ClassicDark() } val LocalType = compositionLocalOf { sessionTypography } -var cachedColors: MaybeFollowSystemColors? = null +var cachedColorsProvider: ThemeColorsProvider? = null /** * Apply a Material2 compose theme based on user selections in SharedPreferences. @@ -32,7 +32,7 @@ fun SessionMaterialTheme( val context = LocalContext.current val preferences = AppTextSecurePreferences(context) - val cachedColors = cachedColors ?: preferences.colors.also { cachedColors = it } + val cachedColors = cachedColorsProvider ?: preferences.getColorsProvider().also { cachedColorsProvider = it } SessionMaterialTheme( colors = cachedColors.get(), From ce501fd36389ded62e9901daac21b9fc29b31b23 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Tue, 30 Jul 2024 00:19:16 +0930 Subject: [PATCH 19/45] Move invalidateComposeThemeColors() --- .../preferences/appearance/AppearanceSettingsViewModel.kt | 7 +------ .../java/org/thoughtcrime/securesms/ui/theme/Themes.kt | 5 +++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt index 47cc9f69bf..2547e23e22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/appearance/AppearanceSettingsViewModel.kt @@ -6,7 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import org.session.libsession.utilities.TextSecurePreferences -import org.thoughtcrime.securesms.ui.theme.cachedColorsProvider +import org.thoughtcrime.securesms.ui.theme.invalidateComposeThemeColors import org.thoughtcrime.securesms.util.ThemeState import org.thoughtcrime.securesms.util.themeState import javax.inject.Inject @@ -17,11 +17,6 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec private val _uiState = MutableStateFlow(prefs.themeState()) val uiState: StateFlow = _uiState - fun invalidateComposeThemeColors() { - // invalidate compose theme colors - cachedColorsProvider = null - } - fun setNewAccent(@StyleRes newAccentColorStyle: Int) { prefs.setAccentColorStyle(newAccentColorStyle) // update UI state diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt index 9b83028ec8..2f4957565b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/Themes.kt @@ -22,6 +22,11 @@ val LocalType = compositionLocalOf { sessionTypography } var cachedColorsProvider: ThemeColorsProvider? = null +fun invalidateComposeThemeColors() { + // invalidate compose theme colors + cachedColorsProvider = null +} + /** * Apply a Material2 compose theme based on user selections in SharedPreferences. */ From 35a9f9fbbe0d0a358d66d5bad313f68ac1a0473d Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 30 Jul 2024 12:02:56 +1000 Subject: [PATCH 20/45] Version fetching API added --- .../securesms/util/VersionUtil.kt | 44 ++++++++++++++ .../messaging/file_server/FileServerApi.kt | 57 ++++++++++++++++++- .../messaging/file_server/VersionData.kt | 7 +++ .../libsession/snode/utilities/PromiseUtil.kt | 13 +++++ .../utilities/TextSecurePreferences.kt | 13 +++++ 5 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt create mode 100644 libsession/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt create mode 100644 libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt new file mode 100644 index 0000000000..f5cde8d445 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.util + +import android.content.Context +import android.os.Handler +import android.os.Looper +import org.session.libsession.utilities.TextSecurePreferences + +class VersionUtil( + private val context: Context, + private val prefs: TextSecurePreferences +) { + + private val handler = Handler(Looper.getMainLooper()) + private val runnable: Runnable + + init { + runnable = Runnable { + // Task to be executed every 4 hours + fetchVersionData() + } + + // Re-schedule the task + handler.postDelayed(runnable, FOUR_HOURS) + } + + fun startTimedVersionCheck() { + handler.post(runnable) + } + + fun stopTimedVersionCheck() { + handler.removeCallbacks(runnable) + } + + private fun fetchVersionData() { + // only perform this if at least 4h has elapsed since th last successful check + if(prefs.getLastVersionCheck() < FOUR_HOURS) return + + + } + + companion object { + private const val FOUR_HOURS = 4 * 60 * 60 * 1000L // 4 hours in milliseconds + } +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index 5bffed57ee..2543983cd9 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -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 { @@ -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( @@ -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 + 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) + } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt new file mode 100644 index 0000000000..b7c28020e7 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/VersionData.kt @@ -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. +) \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt b/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt new file mode 100644 index 0000000000..9a4cd0dd5a --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt @@ -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 Promise.await(): T { + return suspendCoroutine { cont -> + success { cont.resume(it) } + fail { cont.resumeWithException(it) } + } +} \ No newline at end of file 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 12fdd4ceaf..5bf109843d 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -13,6 +13,7 @@ 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 @@ -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.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 @@ -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 @@ -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" @@ -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 From d3990572a046a219d82975f1bb41ff3f8e48977e Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 30 Jul 2024 12:46:13 +1000 Subject: [PATCH 21/45] Linking Version util to the app --- .../securesms/ApplicationContext.java | 9 ++++++ .../securesms/util/VersionUtil.kt | 30 +++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 023ec6b660..498060f59e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.sskenvironment.ProfileManager; 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; @@ -142,6 +143,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO private HandlerThread conversationListHandlerThread; private Handler conversationListHandler; private PersistentLogger persistentLogger; + private VersionUtil versionUtil; @Inject LokiAPIDatabase lokiAPIDatabase; @Inject public Storage storage; @@ -248,6 +250,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO resubmitProfilePictureIfNeeded(); loadEmojiSearchIndexIfNeeded(); EmojiSource.refresh(); + versionUtil = new VersionUtil(this, textSecurePreferences); NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create(); HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet); @@ -274,6 +277,10 @@ public class ApplicationContext extends Application implements DefaultLifecycleO OpenGroupManager.INSTANCE.startPolling(); }); + + // try to fetch last version now and start the version polling + versionUtil.fetchVersionData(); + versionUtil.startTimedVersionCheck(); } @Override @@ -286,12 +293,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO poller.stopIfNeeded(); } ClosedGroupPollerV2.getShared().stopAll(); + versionUtil.stopTimedVersionCheck(); } @Override public void onTerminate() { stopKovenant(); // Loki OpenGroupManager.INSTANCE.stopPolling(); + versionUtil.clear(); super.onTerminate(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt index f5cde8d445..68da185baf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt @@ -3,6 +3,12 @@ package org.thoughtcrime.securesms.util import android.content.Context import android.os.Handler import android.os.Looper +import android.util.Log +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 class VersionUtil( @@ -13,6 +19,9 @@ class VersionUtil( private val handler = Handler(Looper.getMainLooper()) private val runnable: Runnable + private val scope = CoroutineScope(Dispatchers.Default) + private var job: Job? = null + init { runnable = Runnable { // Task to be executed every 4 hours @@ -31,11 +40,28 @@ class VersionUtil( handler.removeCallbacks(runnable) } - private fun fetchVersionData() { + fun clear() { + job?.cancel() + stopTimedVersionCheck() + } + + fun fetchVersionData() { + Log.d("", "***** Trying to fetch version. Last check: ${prefs.getLastVersionCheck()}") // only perform this if at least 4h has elapsed since th last successful check if(prefs.getLastVersionCheck() < FOUR_HOURS) return - + job = scope.launch { + try { + // perform the version check + Log.d("", "***** Fetching last version") + val clientVersion = FileServerApi.getClientVersion() + Log.d("", "***** Got version: $clientVersion") + prefs.setLastVersionCheck() + } catch (e: Exception) { + // we can silently ignore the error + Log.e("", "***** Error fetching version", e) + } + } } companion object { From 42733c910d3510248698634860481326a859b7ee Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 30 Jul 2024 13:30:58 +1000 Subject: [PATCH 22/45] Clean up logic Fixed randomly found timeunit error --- .../securesms/ApplicationContext.java | 5 ++- .../org/thoughtcrime/securesms/MuteDialog.kt | 2 +- .../securesms/util/VersionUtil.kt | 32 ++++++++----------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 498060f59e..cfdc16e06c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -250,7 +250,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO resubmitProfilePictureIfNeeded(); loadEmojiSearchIndexIfNeeded(); EmojiSource.refresh(); - versionUtil = new VersionUtil(this, textSecurePreferences); + versionUtil = new VersionUtil(textSecurePreferences); NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create(); HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet); @@ -278,8 +278,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO OpenGroupManager.INSTANCE.startPolling(); }); - // try to fetch last version now and start the version polling - versionUtil.fetchVersionData(); + // fetch last version data versionUtil.startTimedVersionCheck(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt index f294e387ff..071da43311 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.kt @@ -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)), 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 }); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt index 68da185baf..9781c1105a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt @@ -1,20 +1,19 @@ package org.thoughtcrime.securesms.util -import android.content.Context import android.os.Handler import android.os.Looper -import android.util.Log 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 context: Context, private val prefs: TextSecurePreferences ) { + private val FOUR_HOURS: Long = TimeUnit.HOURS.toMillis(4) private val handler = Handler(Looper.getMainLooper()) private val runnable: Runnable @@ -24,12 +23,8 @@ class VersionUtil( init { runnable = Runnable { - // Task to be executed every 4 hours - fetchVersionData() + fetchAndScheduleNextVersionCheck() } - - // Re-schedule the task - handler.postDelayed(runnable, FOUR_HOURS) } fun startTimedVersionCheck() { @@ -45,26 +40,25 @@ class VersionUtil( stopTimedVersionCheck() } - fun fetchVersionData() { - Log.d("", "***** Trying to fetch version. Last check: ${prefs.getLastVersionCheck()}") - // only perform this if at least 4h has elapsed since th last successful check - if(prefs.getLastVersionCheck() < FOUR_HOURS) return + 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 - Log.d("", "***** Fetching last version") val clientVersion = FileServerApi.getClientVersion() - Log.d("", "***** Got version: $clientVersion") prefs.setLastVersionCheck() } catch (e: Exception) { // we can silently ignore the error - Log.e("", "***** Error fetching version", e) } } } - - companion object { - private const val FOUR_HOURS = 4 * 60 * 60 * 1000L // 4 hours in milliseconds - } } \ No newline at end of file From 4b87e926c4845d61e7d91c0f118c575a1012ef49 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 30 Jul 2024 13:58:35 +1000 Subject: [PATCH 23/45] Added a log so we can see when the version data is returned --- .../main/java/org/thoughtcrime/securesms/util/VersionUtil.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt index 9781c1105a..68a5982da5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt @@ -8,11 +8,13 @@ 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()) @@ -55,9 +57,11 @@ class VersionUtil( 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") } } } From 83ea71de246ce3d68319632ac80fa75886ef5106 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 30 Jul 2024 14:49:45 +1000 Subject: [PATCH 24/45] Update app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt Co-authored-by: Andrew --- .../main/java/org/thoughtcrime/securesms/util/VersionUtil.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt index 68a5982da5..76075b4ac1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt @@ -50,7 +50,7 @@ class VersionUtil( 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 + if (lastCheck < FOUR_HOURS) return job?.cancel() job = scope.launch { From 7a8e1309fb461bc867ea944ff9e656d0c474d9ac Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 30 Jul 2024 15:04:41 +1000 Subject: [PATCH 25/45] PR feedback --- .../messenger/libsession_util/util/BlindKeyAPI.kt | 7 ++++++- .../messaging/file_server/FileServerApi.kt | 13 +++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt index 8f57caf071..cd3dac3af2 100644 --- a/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt +++ b/libsession-util/src/main/java/network/loki/messenger/libsession_util/util/BlindKeyAPI.kt @@ -1,10 +1,15 @@ package network.loki.messenger.libsession_util.util object BlindKeyAPI { - init { + 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 } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index 2543983cd9..fb9c014a15 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -151,11 +151,12 @@ object FileServerApi { 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) + 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 + ) + } } } \ No newline at end of file From a594952832ea94db4323a7c0b52327ae9ec0d8ff Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Tue, 30 Jul 2024 15:14:09 +1000 Subject: [PATCH 26/45] Update libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt Co-authored-by: Andrew --- .../org/session/libsession/snode/utilities/PromiseUtil.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt b/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt index 9a4cd0dd5a..9c55a2282a 100644 --- a/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt +++ b/libsession/src/main/java/org/session/libsession/snode/utilities/PromiseUtil.kt @@ -7,7 +7,7 @@ import kotlin.coroutines.suspendCoroutine suspend fun Promise.await(): T { return suspendCoroutine { cont -> - success { cont.resume(it) } - fail { cont.resumeWithException(it) } + success(cont::resume) + fail(cont::resumeWithException) } } \ No newline at end of file From 7fa3d9f3ff5311bbcb7b535ab22ca96798efa259 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Wed, 31 Jul 2024 13:53:54 +0930 Subject: [PATCH 27/45] Fix version check --- .../securesms/ApplicationContext.java | 12 ++-- .../manager/CreateAccountManager.kt | 4 ++ .../onboarding/manager/LoadAccountManager.kt | 6 +- .../{VersionUtil.kt => VersionDataFetcher.kt} | 58 +++++++++---------- .../messaging/file_server/FileServerApi.kt | 4 +- 5 files changed, 42 insertions(+), 42 deletions(-) rename app/src/main/java/org/thoughtcrime/securesms/util/{VersionUtil.kt => VersionDataFetcher.kt} (50%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index cfdc16e06c..a50e003c65 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -86,7 +86,7 @@ import org.thoughtcrime.securesms.sskenvironment.ProfileManager; 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.VersionDataFetcher; import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper; import org.thoughtcrime.securesms.webrtc.CallMessageProcessor; import org.webrtc.PeerConnectionFactory; @@ -111,7 +111,6 @@ import javax.inject.Inject; import dagger.hilt.EntryPoints; import dagger.hilt.android.HiltAndroidApp; import kotlin.Unit; -import kotlinx.coroutines.Job; import network.loki.messenger.BuildConfig; import network.loki.messenger.libsession_util.ConfigBase; import network.loki.messenger.libsession_util.UserProfile; @@ -143,7 +142,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO private HandlerThread conversationListHandlerThread; private Handler conversationListHandler; private PersistentLogger persistentLogger; - private VersionUtil versionUtil; @Inject LokiAPIDatabase lokiAPIDatabase; @Inject public Storage storage; @@ -153,6 +151,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO @Inject PushRegistry pushRegistry; @Inject ConfigFactory configFactory; @Inject LastSentTimestampCache lastSentTimestampCache; + @Inject VersionDataFetcher versionDataFetcher; CallMessageProcessor callMessageProcessor; MessagingModuleConfiguration messagingModuleConfiguration; @@ -250,7 +249,6 @@ public class ApplicationContext extends Application implements DefaultLifecycleO resubmitProfilePictureIfNeeded(); loadEmojiSearchIndexIfNeeded(); EmojiSource.refresh(); - versionUtil = new VersionUtil(textSecurePreferences); NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create(); HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet); @@ -279,7 +277,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO }); // fetch last version data - versionUtil.startTimedVersionCheck(); + versionDataFetcher.startTimedVersionCheck(); } @Override @@ -292,14 +290,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO poller.stopIfNeeded(); } ClosedGroupPollerV2.getShared().stopAll(); - versionUtil.stopTimedVersionCheck(); + versionDataFetcher.stopTimedVersionCheck(); } @Override public void onTerminate() { stopKovenant(); // Loki OpenGroupManager.INSTANCE.stopPolling(); - versionUtil.clear(); + versionDataFetcher.clear(); super.onTerminate(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt index 1e0a21d571..98e9c8b20d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/CreateAccountManager.kt @@ -8,6 +8,7 @@ import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.util.VersionDataFetcher import javax.inject.Inject import javax.inject.Singleton @@ -16,6 +17,7 @@ class CreateAccountManager @Inject constructor( private val application: Application, private val prefs: TextSecurePreferences, private val configFactory: ConfigFactory, + private val versionDataFetcher: VersionDataFetcher ) { private val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage @@ -41,5 +43,7 @@ class CreateAccountManager @Inject constructor( prefs.setLocalRegistrationId(registrationID) prefs.setLocalNumber(userHexEncodedPublicKey) prefs.setRestorationTime(0) + + versionDataFetcher.startTimedVersionCheck() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt index 5a40103830..51d1b24609 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/manager/LoadAccountManager.kt @@ -12,6 +12,7 @@ import org.session.libsignal.utilities.hexEncodedPublicKey import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.util.VersionDataFetcher import javax.inject.Inject import javax.inject.Singleton @@ -19,7 +20,8 @@ import javax.inject.Singleton class LoadAccountManager @Inject constructor( @dagger.hilt.android.qualifiers.ApplicationContext private val context: Context, private val configFactory: ConfigFactory, - private val prefs: TextSecurePreferences + private val prefs: TextSecurePreferences, + private val versionDataFetcher: VersionDataFetcher ) { private val database: LokiAPIDatabaseProtocol get() = SnodeModule.shared.storage @@ -52,6 +54,8 @@ class LoadAccountManager @Inject constructor( setHasViewedSeed(true) } + versionDataFetcher.startTimedVersionCheck() + ApplicationContext.getInstance(context).retrieveUserProfile() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt similarity index 50% rename from app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt rename to app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt index 76075b4ac1..48be478c95 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt @@ -4,33 +4,36 @@ 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 +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.time.Duration.Companion.hours -class VersionUtil( +private val TAG: String = VersionDataFetcher::class.java.simpleName +private val REFRESH_TIME_MS = 4.hours.inWholeMilliseconds + +@Singleton +class VersionDataFetcher @Inject constructor( 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() - } + private val runnable = Runnable { + fetchVersionData() } + private val scope = CoroutineScope(Dispatchers.Default) + fun startTimedVersionCheck() { - handler.post(runnable) + stopTimedVersionCheck() + + // Call immediately if 4h or more has elapsed since the last successful check else schedule. + handler.postDelayed( + runnable, + REFRESH_TIME_MS + prefs.getLastVersionCheck() - System.currentTimeMillis() + ) } fun stopTimedVersionCheck() { @@ -38,31 +41,22 @@ class VersionUtil( } 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 { + scope.launch { try { - // perform the version check + // 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") + // We can silently ignore the error + Log.e(TAG, "Error fetching version data", e) } + + prefs.setLastVersionCheck() + startTimedVersionCheck() } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index fb9c014a15..a5ebdd5d36 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -16,7 +16,7 @@ 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 +import kotlin.time.Duration.Companion.milliseconds object FileServerApi { @@ -127,7 +127,7 @@ object FileServerApi { ?: throw (Error.NoEd25519KeyPair) val blindedKeys = BlindKeyAPI.blindVersionKeyPair(secretKey) - val timestamp = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) // The current timestamp in seconds + val timestamp = System.currentTimeMillis().milliseconds.inWholeSeconds // The current timestamp in seconds val signature = BlindKeyAPI.blindVersionSign(secretKey, timestamp) // The hex encoded version-blinded public key with a 07 prefix From 40db23d341ff7a1e4a40a6199966df05b17b0b74 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Wed, 31 Jul 2024 15:52:38 +0930 Subject: [PATCH 28/45] Fix setLastVersion called onException --- .../securesms/ApplicationContext.java | 2 +- .../securesms/util/VersionDataFetcher.kt | 49 ++++++++----------- 2 files changed, 22 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index a50e003c65..daa159501b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -297,7 +297,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO public void onTerminate() { stopKovenant(); // Loki OpenGroupManager.INSTANCE.stopPolling(); - versionDataFetcher.clear(); + versionDataFetcher.stopTimedVersionCheck(); super.onTerminate(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt index 48be478c95..2868e4e8db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt @@ -20,43 +20,36 @@ class VersionDataFetcher @Inject constructor( private val prefs: TextSecurePreferences ) { private val handler = Handler(Looper.getMainLooper()) - private val runnable = Runnable { - fetchVersionData() - } - - private val scope = CoroutineScope(Dispatchers.Default) - - fun startTimedVersionCheck() { - stopTimedVersionCheck() - - // Call immediately if 4h or more has elapsed since the last successful check else schedule. - handler.postDelayed( - runnable, - REFRESH_TIME_MS + prefs.getLastVersionCheck() - System.currentTimeMillis() - ) - } - - fun stopTimedVersionCheck() { - handler.removeCallbacks(runnable) - } - - fun clear() { - stopTimedVersionCheck() - } - - private fun fetchVersionData() { + private val fetchVersionData = Runnable { scope.launch { try { // Perform the version check val clientVersion = FileServerApi.getClientVersion() Log.i(TAG, "Fetched version data: $clientVersion") + prefs.setLastVersionCheck() + startTimedVersionCheck() } catch (e: Exception) { // We can silently ignore the error Log.e(TAG, "Error fetching version data", e) + // Schedule the next check for 4 hours from now, but do not setLastVersionCheck + // so the app will retry when the app is next foregrounded. + startTimedVersionCheck(REFRESH_TIME_MS) } - - prefs.setLastVersionCheck() - startTimedVersionCheck() } } + + private val scope = CoroutineScope(Dispatchers.Default) + + fun startTimedVersionCheck( + delayMillis: Long = REFRESH_TIME_MS + prefs.getLastVersionCheck() - System.currentTimeMillis() + ) { + stopTimedVersionCheck() + + // Call immediately if 4h or more has elapsed since the last successful check else schedule. + handler.postDelayed(fetchVersionData, delayMillis) + } + + fun stopTimedVersionCheck() { + handler.removeCallbacks(fetchVersionData) + } } \ No newline at end of file From 4992123bafcd2a40a9f5b5c4e822f87a57b0ff6f Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Wed, 31 Jul 2024 16:00:23 +0930 Subject: [PATCH 29/45] Cleanup comment --- .../org/thoughtcrime/securesms/util/VersionDataFetcher.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt index 2868e4e8db..20f8ca5308 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt @@ -40,12 +40,16 @@ class VersionDataFetcher @Inject constructor( private val scope = CoroutineScope(Dispatchers.Default) + /** + * Schedules fetching version data [delayMillis] milliseconds from now. + * + * This method will fetch immediately if 4 hours or more has elapsed since the last successful + * check. + */ fun startTimedVersionCheck( delayMillis: Long = REFRESH_TIME_MS + prefs.getLastVersionCheck() - System.currentTimeMillis() ) { stopTimedVersionCheck() - - // Call immediately if 4h or more has elapsed since the last successful check else schedule. handler.postDelayed(fetchVersionData, delayMillis) } From 72d77a9caf11df94e0e385206fffec8b9f3d8c4f Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Wed, 31 Jul 2024 17:40:47 +0930 Subject: [PATCH 30/45] Add @JvmOverloads --- .../java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt index 20f8ca5308..77588c21be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt @@ -46,6 +46,7 @@ class VersionDataFetcher @Inject constructor( * This method will fetch immediately if 4 hours or more has elapsed since the last successful * check. */ + @JvmOverloads fun startTimedVersionCheck( delayMillis: Long = REFRESH_TIME_MS + prefs.getLastVersionCheck() - System.currentTimeMillis() ) { From 8d15169c93e823222e7bb2bbd1cf97a1a4def190 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 1 Aug 2024 09:03:13 +1000 Subject: [PATCH 31/45] Version bump to go above last live version --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index c526c02d1c..f8fa9e2c2f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,7 +31,7 @@ configurations.all { exclude module: "commons-logging" } -def canonicalVersionCode = 376 +def canonicalVersionCode = 377 def canonicalVersionName = "1.19.0" def postFixSize = 10 From 5c4e95c4ec66abb2ef3387732204be690c158368 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Thu, 1 Aug 2024 10:34:45 +0930 Subject: [PATCH 32/45] Update VersionDataFetcher KDoc --- .../org/thoughtcrime/securesms/util/VersionDataFetcher.kt | 6 +++--- .../libsession/messaging/file_server/FileServerApi.kt | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt index 77588c21be..aba814524c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionDataFetcher.kt @@ -41,10 +41,10 @@ class VersionDataFetcher @Inject constructor( private val scope = CoroutineScope(Dispatchers.Default) /** - * Schedules fetching version data [delayMillis] milliseconds from now. + * Schedules fetching version data. * - * This method will fetch immediately if 4 hours or more has elapsed since the last successful - * check. + * @param delayMillis The delay before fetching version data. Default value is 4 hours from the + * last check or 0 if there was no previous check or if it was longer than 4 hours ago. */ @JvmOverloads fun startTimedVersionCheck( diff --git a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt index a5ebdd5d36..6a9c95aa56 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/file_server/FileServerApi.kt @@ -131,10 +131,7 @@ object FileServerApi { 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 blindedPkHex = "07" + blindedKeys.pubKey.toHexString() val request = Request( verb = HTTP.Verb.GET, From b93ec3be04f3370ac5b7d285295a0e142038fe82 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Fri, 2 Aug 2024 12:22:25 +0930 Subject: [PATCH 33/45] Optimise Snode and Snode.Version --- .../securesms/database/LokiAPIDatabase.kt | 35 ++---------- .../org/session/libsession/snode/SnodeAPI.kt | 34 +++++------- .../org/session/libsession/utilities/Util.kt | 33 ++---------- .../org/session/libsignal/utilities/Snode.kt | 54 ++++++++++++++++++- 4 files changed, 73 insertions(+), 83 deletions(-) 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 f1f999242c..ff22ae14e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt @@ -166,8 +166,6 @@ 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 } @@ -175,15 +173,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( val database = databaseHelper.readableDatabase return database.get(snodePoolTable, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor -> val snodePoolAsString = cursor.getString(cursor.getColumnIndexOrThrow(snodePool)) - snodePoolAsString.split(", ").mapNotNull { snodeAsString -> - val components = snodeAsString.split("-") - val address = components[0] - 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 - val version = components.getOrNull(4) ?: EMPTY_VERSION - Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version) - } + snodePoolAsString.split(", ").mapNotNull(::Snode) }?.toSet() ?: setOf() } @@ -231,18 +221,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( val database = databaseHelper.readableDatabase fun get(indexPath: String): Snode? { return database.get(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor -> - val snodeAsString = cursor.getString(cursor.getColumnIndexOrThrow(snode)) - val components = snodeAsString.split("-") - val address = components[0] - 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), version) - } else { - null - } + Snode(cursor.getString(cursor.getColumnIndexOrThrow(snode))) } } val result = mutableListOf>() @@ -276,15 +255,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( val database = databaseHelper.readableDatabase return database.get(swarmTable, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor -> val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm)) - swarmAsString.split(", ").mapNotNull { targetAsString -> - val components = targetAsString.split("-") - val address = components[0] - 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 - val version = components.getOrNull(4) ?: EMPTY_VERSION - Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version) - } + swarmAsString.split(", ").mapNotNull(::Snode) }?.toSet() } 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 034ef0e7d2..9ceefe8386 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -18,6 +18,7 @@ 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.libsession.utilities.toByteArray import org.session.libsignal.crypto.getRandomElement import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.protos.SignalServiceProtos @@ -94,8 +95,6 @@ object SnodeAPI { 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.") @@ -191,7 +190,7 @@ object SnodeAPI { 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 + ?.let(Snode::Version) // turn it int a version if (address != null && port != null && ed25519Key != null && x25519Key != null && address != "0.0.0.0" && version != null) { @@ -696,7 +695,7 @@ object SnodeAPI { getSingleTargetSnode(publicKey).bind { snode -> retryIfNeeded(maxRetryCount) { val signature = ByteArray(Sign.BYTES) - val verificationData = (Snode.Method.DeleteMessage.rawValue + serverHashes.fold("") { a, v -> a + v }).toByteArray() + val verificationData = sequenceOf(Snode.Method.DeleteMessage.rawValue).plus(serverHashes).toByteArray() sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes) val deleteMessageParams = mapOf( "pubkey" to userPublicKey, @@ -719,7 +718,7 @@ object SnodeAPI { val signature = json["signature"] as String val snodePublicKey = Key.fromHexString(hexSnodePublicKey) // The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] ) - val message = (userPublicKey + serverHashes.fold("") { a, v -> a + v } + hashes.fold("") { a, v -> a + v }).toByteArray() + val message = sequenceOf(userPublicKey).plus(serverHashes).plus(hashes).toByteArray() sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes) } } @@ -733,11 +732,10 @@ object SnodeAPI { } // Parsing - private fun parseSnodes(rawResponse: Any): List { - val json = rawResponse as? Map<*, *> - val rawSnodes = json?.get("snodes") as? List<*> - if (rawSnodes != null) { - return rawSnodes.mapNotNull { rawSnode -> + private fun parseSnodes(rawResponse: Any): List = + (rawResponse as? Map<*, *>) + ?.run { get("snodes") as? List<*> } + ?.mapNotNull { rawSnode -> val rawSnodeAsJSON = rawSnode as? Map<*, *> val address = rawSnodeAsJSON?.get("ip") as? String val portAsString = rawSnodeAsJSON?.get("port") as? String @@ -746,17 +744,12 @@ object SnodeAPI { 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), EMPTY_VERSION) + Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key), Snode.Version.ZERO) } else { Log.d("Loki", "Failed to parse snode from: ${rawSnode?.prettifiedDescription()}.") null } - } - } else { - Log.d("Loki", "Failed to parse snodes from: ${rawResponse.prettifiedDescription()}.") - return listOf() - } - } + } ?: listOf().also { Log.d("Loki", "Failed to parse snodes from: ${rawResponse.prettifiedDescription()}.") } fun deleteAllMessages(): Promise, Exception> { return retryIfNeeded(maxRetryCount) { @@ -796,8 +789,7 @@ object SnodeAPI { getSingleTargetSnode(userPublicKey).bind { snode -> retryIfNeeded(maxRetryCount) { // "expire" || expiry || messages[0] || ... || messages[N] - val verificationData = - (Snode.Method.Expire.rawValue + updatedExpiryMsWithNetworkOffset + serverHashes.fold("") { a, v -> a + v }).toByteArray() + val verificationData = sequenceOf(Snode.Method.Expire.rawValue, "$updatedExpiryMsWithNetworkOffset").plus(serverHashes).toByteArray() val signature = ByteArray(Sign.BYTES) sodium.cryptoSignDetached( signature, @@ -828,7 +820,7 @@ object SnodeAPI { val signature = json["signature"] as String val snodePublicKey = Key.fromHexString(hexSnodePublicKey) // The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] ) - val message = (userPublicKey + serverHashes.fold("") { a, v -> a + v } + hashes.fold("") { a, v -> a + v }).toByteArray() + val message = sequenceOf(userPublicKey).plus(serverHashes).plus(hashes).toByteArray() if (sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)) { hashes to expiryApplied } else listOf() to 0L @@ -922,7 +914,7 @@ object SnodeAPI { val signature = json["signature"] as String val snodePublicKey = Key.fromHexString(hexSnodePublicKey) // The signature looks like ( PUBKEY_HEX || TIMESTAMP || DELETEDHASH[0] || ... || DELETEDHASH[N] ) - val message = (userPublicKey + timestamp.toString() + hashes.joinToString(separator = "")).toByteArray() + val message = sequenceOf(userPublicKey, "$timestamp").plus(hashes).toByteArray() sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes) } } 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 929f53e305..d47754b7ed 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/Util.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/Util.kt @@ -366,34 +366,6 @@ 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 @@ -440,3 +412,8 @@ fun Iterable.associateByNotNull( inline fun Iterable.groupByNotNull(keySelector: (E) -> K?): Map> = LinkedHashMap>().also { forEach { e -> keySelector(e)?.let { k -> it.getOrPut(k) { mutableListOf() } += e } } } + +fun Sequence.toByteArray(): ByteArray = ByteArrayOutputStream().use { output -> + forEach { it.byteInputStream().use { input -> input.copyTo(output) } } + output.toByteArray() +} 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 f6b11754ad..cc123a8527 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt @@ -1,9 +1,21 @@ package org.session.libsignal.utilities -class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val version: String) { +import android.annotation.SuppressLint + +fun Snode(string: String): Snode? { + val components = string.split("-") + val address = components[0] + val port = components.getOrNull(1)?.toIntOrNull() ?: return null + val ed25519Key = components.getOrNull(2) ?: return null + val x25519Key = components.getOrNull(3) ?: return null + val version = components.getOrNull(4)?.let(Snode::Version) ?: Snode.Version.ZERO + return Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version) +} + +class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val version: Version) { val ip: String get() = address.removePrefix("https://") - public enum class Method(val rawValue: String) { + enum class Method(val rawValue: String) { GetSwarm("get_snodes_for_pubkey"), Retrieve("retrieve"), SendMessage("store"), @@ -32,4 +44,42 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val v } override fun toString(): String { return "$address:$port" } + + companion object { + private val CACHE = mutableMapOf() + + @SuppressLint("NotConstructor") + fun Version(value: String) = CACHE.getOrElse(value) { + Snode.Version(value) + } + } + + @JvmInline + value class Version(val value: ULong) { + companion object { + val ZERO = Version(0UL) + private const val MASK_BITS = 16 + private const val MASK = 0xFFFFUL + + private fun Sequence.foldToVersionAsULong() = take(4).foldIndexed(0UL) { i, acc, it -> + it and MASK shl (3 - i) * MASK_BITS or acc + } + } + + constructor(parts: List): this( + parts.asSequence() + .map { it.toByte().toULong() } + .foldToVersionAsULong() + ) + + constructor(value: Int): this(value.toULong()) + + internal constructor(value: String): this( + value.splitToSequence(".") + .map { it.toULongOrNull() ?: 0UL } + .foldToVersionAsULong() + ) + + operator fun compareTo(other: Version): Int = value.compareTo(other.value) + } } From 482f169df1f5fde73624624de86504f92ccd83a1 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Sat, 3 Aug 2024 02:46:42 +0930 Subject: [PATCH 34/45] Refactor SnodeApi --- .../messaging/jobs/ConfigurationSyncJob.kt | 1 - .../org/session/libsession/snode/SnodeAPI.kt | 726 +++++++----------- .../session/libsignal/utilities/Base64.java | 4 +- 3 files changed, 268 insertions(+), 463 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt index 4a3299d197..a9f076b106 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/ConfigurationSyncJob.kt @@ -63,7 +63,6 @@ data class ConfigurationSyncJob(val destination: Destination): Job { // return a list of batch request objects val snodeMessage = MessageSender.buildConfigMessageToSnode(destination.destinationPublicKey(), message) val authenticated = SnodeAPI.buildAuthenticatedStoreBatchInfo( - destination.destinationPublicKey(), config.configNamespace(), snodeMessage ) ?: return@map null // this entry will be null otherwise 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 9ceefe8386..8e19234b0d 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -9,9 +9,9 @@ import com.goterl.lazysodium.interfaces.PwHash import com.goterl.lazysodium.interfaces.SecretBox import com.goterl.lazysodium.interfaces.Sign import com.goterl.lazysodium.utils.Key +import com.goterl.lazysodium.utils.KeyPair import nl.komponents.kovenant.Promise import nl.komponents.kovenant.all -import nl.komponents.kovenant.deferred import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.task @@ -30,7 +30,6 @@ import org.session.libsignal.utilities.JsonUtil 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.prettifiedDescription import org.session.libsignal.utilities.retryIfNeeded import java.security.SecureRandom @@ -46,7 +45,7 @@ object SnodeAPI { private val broadcaster: Broadcaster get() = SnodeModule.shared.broadcaster - internal var snodeFailureCount: MutableMap = mutableMapOf() + private var snodeFailureCount: MutableMap = mutableMapOf() internal var snodePool: Set get() = database.getSnodePool() set(newValue) { database.setSnodePool(newValue) } @@ -57,7 +56,7 @@ object SnodeAPI { internal var clockOffset = 0L @JvmStatic - public val nowWithOffset + val nowWithOffset get() = System.currentTimeMillis() + clockOffset internal var forkInfo by observable(database.getForkInfo()) { _, oldValue, newValue -> @@ -68,32 +67,32 @@ object SnodeAPI { } // Settings - private val maxRetryCount = 6 - private val minimumSnodePoolCount = 12 - private val minimumSwarmSnodeCount = 3 + private const val maxRetryCount = 6 + private const val minimumSnodePoolCount = 12 + private const val minimumSwarmSnodeCount = 3 // Use port 4433 if the API level can handle the network security configuration and enforce pinned certificates private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4443 - private val seedNodePool by lazy { - if (useTestnet) { - setOf( "http://public.loki.foundation:38157" ) - } else { - setOf( - "https://seed1.getsession.org:$seedNodePort", - "https://seed2.getsession.org:$seedNodePort", - "https://seed3.getsession.org:$seedNodePort", - ) - } - } + private const val snodeFailureThreshold = 3 private const val useOnionRequests = true - const val useTestnet = false + private 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" + private val seedNodePool = if (useTestnet) { + setOf( "http://public.loki.foundation:38157" ) + } else { + setOf( + "https://seed1.getsession.org:$seedNodePort", + "https://seed2.getsession.org:$seedNodePort", + "https://seed3.getsession.org:$seedNodePort", + ) + } + + private const val KEY_IP = "public_ip" + private const val KEY_PORT = "storage_port" + private const val KEY_X25519 = "pubkey_x25519" + private const val KEY_ED25519 = "pubkey_ed25519" + private const val KEY_VERSION = "storage_server_version" // Error sealed class Error(val description: String) : Exception(description) { @@ -122,39 +121,28 @@ object SnodeAPI { parameters: Map, publicKey: String? = null, version: Version = Version.V3 - ): RawResponsePromise { - val url = "${snode.address}:${snode.port}/storage_rpc/v1" - val deferred = deferred, Exception>() - if (useOnionRequests) { - OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey).map { - val body = it.body ?: throw Error.Generic - deferred.resolve(JsonUtil.fromJson(body, Map::class.java)) - }.fail { deferred.reject(it) } - } else { - ThreadUtils.queue { - val payload = mapOf( "method" to method.rawValue, "params" to parameters ) - try { - val response = HTTP.execute(HTTP.Verb.POST, url, payload).toString() - val json = JsonUtil.fromJson(response, Map::class.java) - deferred.resolve(json) - } catch (exception: Exception) { - val httpRequestFailedException = exception as? HTTP.HTTPRequestFailedException - if (httpRequestFailedException != null) { - val error = handleSnodeError(httpRequestFailedException.statusCode, httpRequestFailedException.json, snode, publicKey) - if (error != null) { return@queue deferred.reject(exception) } - } - Log.d("Loki", "Unhandled exception: $exception.") - deferred.reject(exception) + ): RawResponsePromise = if (useOnionRequests) OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey).map { + val body = it.body ?: throw Error.Generic + JsonUtil.fromJson(body, Map::class.java) + } else task { + val payload = mapOf( "method" to method.rawValue, "params" to parameters ) + try { + val url = "${snode.address}:${snode.port}/storage_rpc/v1" + val response = HTTP.execute(HTTP.Verb.POST, url, payload).toString() + JsonUtil.fromJson(response, Map::class.java) + } catch (exception: Exception) { + (exception as? HTTP.HTTPRequestFailedException)?.run { + handleSnodeError(statusCode, json, snode, publicKey) + // TODO Check if we meant to throw the error returned by handleSnodeError + throw exception } + Log.d("Loki", "Unhandled exception: $exception.") + throw exception } } - return deferred.promise - } - internal fun getRandomSnode(): Promise { - val snodePool = this.snodePool - - if (snodePool.count() < minimumSnodePoolCount) { + internal fun getRandomSnode(): Promise = + snodePool.takeIf { it.size >= minimumSnodePoolCount }?.let { Promise.of(it.getRandomElement()) } ?: task { val target = seedNodePool.random() val url = "$target/json_rpc" Log.d("Loki", "Populating snode pool using: $target.") @@ -169,73 +157,48 @@ object SnodeAPI { ) ) ) - val deferred = deferred() - deferred() - ThreadUtils.queue { - try { - val response = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true) - val json = try { - JsonUtil.fromJson(response, Map::class.java) - } catch (exception: Exception) { - mapOf( "result" to response.toString()) - } - val intermediate = json["result"] as? Map<*, *> - val rawSnodes = intermediate?.get("service_node_states") as? List<*> - if (rawSnodes != null) { - val snodePool = rawSnodes.mapNotNull { rawSnode -> - val rawSnodeAsJSON = rawSnode as? Map<*, *> - 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 - ?.let(Snode::Version) // turn it int a version - - 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 - } - }.toMutableSet() - Log.d("Loki", "Persisting snode pool to database.") - this.snodePool = snodePool - try { - deferred.resolve(snodePool.getRandomElement()) - } catch (exception: Exception) { - Log.d("Loki", "Got an empty snode pool from: $target.") - deferred.reject(SnodeAPI.Error.Generic) - } - } else { - Log.d("Loki", "Failed to update snode pool from: ${(rawSnodes as List<*>?)?.prettifiedDescription()}.") - deferred.reject(SnodeAPI.Error.Generic) - } - } catch (exception: Exception) { - deferred.reject(exception) - } + val response = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true) + val json = try { + JsonUtil.fromJson(response, Map::class.java) + } catch (exception: Exception) { + mapOf( "result" to response.toString()) } - return deferred.promise - } else { - return Promise.of(snodePool.getRandomElement()) - } - } + val intermediate = json["result"] as? Map<*, *> ?: throw Error.Generic + .also { Log.d("Loki", "Failed to update snode pool, intermediate was null.") } + val rawSnodes = intermediate["service_node_states"] as? List<*> ?: throw Error.Generic + .also { Log.d("Loki", "Failed to update snode pool, rawSnodes was null.") } - private fun extractVersionString(jsonVersion: String): String{ - return jsonVersion.removeSurrounding("[", "]").split(", ").joinToString(separator = ".") + rawSnodes.asSequence().mapNotNull { it as? Map<*, *> }.mapNotNull { rawSnode -> + createSnode( + address = rawSnode[KEY_IP] as? String, + port = rawSnode[KEY_PORT] as? Int, + ed25519Key = rawSnode[KEY_ED25519] as? String, + x25519Key = rawSnode[KEY_X25519] as? String, + version = (rawSnode[KEY_VERSION] as? List<*>) + ?.filterIsInstance() + ?.let(Snode::Version) + ).also { if (it == null) Log.d("Loki", "Failed to parse: ${rawSnode.prettifiedDescription()}.") } + }.toSet().also { + Log.d("Loki", "Persisting snode pool to database.") + this.snodePool = it + }.runCatching { getRandomElement() }.onFailure { + Log.d("Loki", "Got an empty snode pool from: $target.") + throw SnodeAPI.Error.Generic + }.getOrThrow() + } + + private fun createSnode(address: String?, port: Int?, ed25519Key: String?, x25519Key: String?, version: Snode.Version? = Snode.Version.ZERO): Snode? { + return Snode( + address?.takeUnless { it == "0.0.0.0" }?.let { "https://$it" } ?: return null, + port ?: return null, + Snode.KeySet(ed25519Key ?: return null, x25519Key ?: return null), + version ?: return null + ) } internal fun dropSnodeFromSwarmIfNeeded(snode: Snode, publicKey: String) { - val swarm = database.getSwarm(publicKey)?.toMutableSet() - if (swarm != null && swarm.contains(snode)) { - swarm.remove(snode) - database.setSwarm(publicKey, swarm) + database.getSwarm(publicKey)?.takeIf { snode in it }?.let { + database.setSwarm(publicKey, it - snode) } } @@ -246,8 +209,6 @@ object SnodeAPI { // Public API fun getAccountID(onsName: String): Promise { - val deferred = deferred() - val promise = deferred.promise val validationCount = 3 val accountIDByteCount = 33 // Hash the ONS name using BLAKE2b @@ -255,96 +216,79 @@ object SnodeAPI { val nameAsData = onsName.toByteArray() val nameHash = ByteArray(GenericHash.BYTES) if (!sodium.cryptoGenericHash(nameHash, nameHash.size, nameAsData, nameAsData.size.toLong())) { - deferred.reject(Error.HashingFailed) - return promise + throw Error.HashingFailed } val base64EncodedNameHash = Base64.encodeBytes(nameHash) // Ask 3 different snodes for the Account ID associated with the given name hash val parameters = mapOf( - "endpoint" to "ons_resolve", - "params" to mapOf( "type" to 0, "name_hash" to base64EncodedNameHash ) + "endpoint" to "ons_resolve", + "params" to mapOf( "type" to 0, "name_hash" to base64EncodedNameHash ) ) - val promises = (1..validationCount).map { + val promises = List(validationCount) { getRandomSnode().bind { snode -> retryIfNeeded(maxRetryCount) { invoke(Snode.Method.OxenDaemonRPCCall, snode, parameters) } } } - all(promises).success { results -> + return all(promises).map { results -> val accountIDs = mutableListOf() for (json in results) { val intermediate = json["result"] as? Map<*, *> - val hexEncodedCiphertext = intermediate?.get("encrypted_value") as? String - if (hexEncodedCiphertext != null) { - val ciphertext = Hex.fromStringCondensed(hexEncodedCiphertext) - val isArgon2Based = (intermediate["nonce"] == null) - if (isArgon2Based) { - // Handle old Argon2-based encryption used before HF16 - val salt = ByteArray(PwHash.SALTBYTES) - val key: ByteArray - val nonce = ByteArray(SecretBox.NONCEBYTES) - val accountIDAsData = ByteArray(accountIDByteCount) - try { - key = Key.fromHexString(sodium.cryptoPwHash(onsName, SecretBox.KEYBYTES, salt, PwHash.OPSLIMIT_MODERATE, PwHash.MEMLIMIT_MODERATE, PwHash.Alg.PWHASH_ALG_ARGON2ID13)).asBytes - } catch (e: SodiumException) { - deferred.reject(Error.HashingFailed) - return@success - } - if (!sodium.cryptoSecretBoxOpenEasy(accountIDAsData, ciphertext, ciphertext.size.toLong(), nonce, key)) { - deferred.reject(Error.DecryptionFailed) - return@success - } - accountIDs.add(Hex.toStringCondensed(accountIDAsData)) - } else { - val hexEncodedNonce = intermediate["nonce"] as? String - if (hexEncodedNonce == null) { - deferred.reject(Error.Generic) - return@success - } - val nonce = Hex.fromStringCondensed(hexEncodedNonce) - val key = ByteArray(GenericHash.BYTES) - if (!sodium.cryptoGenericHash(key, key.size, nameAsData, nameAsData.size.toLong(), nameHash, nameHash.size)) { - deferred.reject(Error.HashingFailed) - return@success - } - val accountIDAsData = ByteArray(accountIDByteCount) - if (!sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(accountIDAsData, null, null, ciphertext, ciphertext.size.toLong(), null, 0, nonce, key)) { - deferred.reject(Error.DecryptionFailed) - return@success - } - accountIDs.add(Hex.toStringCondensed(accountIDAsData)) + val hexEncodedCiphertext = intermediate?.get("encrypted_value") as? String ?: throw Error.Generic + val ciphertext = Hex.fromStringCondensed(hexEncodedCiphertext) + val isArgon2Based = (intermediate["nonce"] == null) + if (isArgon2Based) { + // Handle old Argon2-based encryption used before HF16 + val salt = ByteArray(PwHash.SALTBYTES) + val nonce = ByteArray(SecretBox.NONCEBYTES) + val accountIDAsData = ByteArray(accountIDByteCount) + val key = try { + Key.fromHexString(sodium.cryptoPwHash(onsName, SecretBox.KEYBYTES, salt, PwHash.OPSLIMIT_MODERATE, PwHash.MEMLIMIT_MODERATE, PwHash.Alg.PWHASH_ALG_ARGON2ID13)).asBytes + } catch (e: SodiumException) { + throw Error.HashingFailed } + if (!sodium.cryptoSecretBoxOpenEasy(accountIDAsData, ciphertext, ciphertext.size.toLong(), nonce, key)) { + throw Error.DecryptionFailed + } + accountIDs.add(Hex.toStringCondensed(accountIDAsData)) } else { - deferred.reject(Error.Generic) - return@success + val hexEncodedNonce = intermediate["nonce"] as? String ?: throw Error.Generic + val nonce = Hex.fromStringCondensed(hexEncodedNonce) + val key = ByteArray(GenericHash.BYTES) + if (!sodium.cryptoGenericHash(key, key.size, nameAsData, nameAsData.size.toLong(), nameHash, nameHash.size)) { + throw Error.HashingFailed + } + val accountIDAsData = ByteArray(accountIDByteCount) + if (!sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(accountIDAsData, null, null, ciphertext, ciphertext.size.toLong(), null, 0, nonce, key)) { + throw Error.DecryptionFailed + } + accountIDs.add(Hex.toStringCondensed(accountIDAsData)) } } - if (accountIDs.size == validationCount && accountIDs.toSet().size == 1) { - deferred.resolve(accountIDs.first()) - } else { - deferred.reject(Error.ValidationFailed) - } + accountIDs.takeIf { it.size == validationCount && it.toSet().size == 1 }?.first() + ?: throw Error.ValidationFailed } - return promise } - fun getSwarm(publicKey: String): Promise, Exception> { - val cachedSwarm = database.getSwarm(publicKey) - return if (cachedSwarm != null && cachedSwarm.size >= minimumSwarmSnodeCount) { - val cachedSwarmCopy = mutableSetOf() // Workaround for a Kotlin compiler issue - cachedSwarmCopy.addAll(cachedSwarm) - task { cachedSwarmCopy } - } else { - val parameters = mapOf( "pubKey" to publicKey ) - getRandomSnode().bind { - invoke(Snode.Method.GetSwarm, it, parameters, publicKey) + fun getSwarm(publicKey: String): Promise, Exception> = + database.getSwarm(publicKey)?.takeIf { it.size >= minimumSwarmSnodeCount }?.let(Promise.Companion::of) + ?: getRandomSnode().bind { + invoke(Snode.Method.GetSwarm, it, parameters = mapOf( "pubKey" to publicKey ), publicKey) }.map { parseSnodes(it).toSet() }.success { database.setSwarm(publicKey, it) } - } + + private fun signAndEncode(data: ByteArray, userED25519KeyPair: KeyPair) = sign(data, userED25519KeyPair).let(Base64::encodeBytes) + private fun sign(data: ByteArray, userED25519KeyPair: KeyPair): ByteArray = ByteArray(Sign.BYTES).also { + sodium.cryptoSignDetached( + it, + data, + data.size.toLong(), + userED25519KeyPair.secretKey.asBytes + ) } fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise { @@ -365,23 +309,19 @@ object SnodeAPI { } val timestamp = System.currentTimeMillis() + clockOffset val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString - val signature = ByteArray(Sign.BYTES) - val verificationData = - if (namespace != 0) "retrieve$namespace$timestamp".toByteArray() - else "retrieve$timestamp".toByteArray() - try { - sodium.cryptoSignDetached( - signature, - verificationData, - verificationData.size.toLong(), - userED25519KeyPair.secretKey.asBytes - ) + val verificationData = buildString { + append("retrieve") + if (namespace != 0) append(namespace) + append(timestamp) + }.toByteArray() + val signature = try { + signAndEncode(verificationData, userED25519KeyPair) } catch (exception: Exception) { return Promise.ofFail(Error.SigningFailed) } parameters["timestamp"] = timestamp parameters["pubkey_ed25519"] = ed25519PublicKey - parameters["signature"] = Base64.encodeBytes(signature) + parameters["signature"] = signature } // If the namespace is default (0) here it will be implicitly read as 0 on the storage server @@ -394,42 +334,34 @@ object SnodeAPI { return invoke(Snode.Method.Retrieve, snode, parameters, publicKey) } - fun buildAuthenticatedStoreBatchInfo(publicKey: String, namespace: Int, message: SnodeMessage): SnodeBatchRequestInfo? { - val params = mutableMapOf() - // load the message data params into the sub request - // currently loads: - // pubKey - // data - // ttl - // timestamp - params.putAll(message.toJSON()) - params["namespace"] = namespace - + fun buildAuthenticatedStoreBatchInfo(namespace: Int, message: SnodeMessage): SnodeBatchRequestInfo? { // used for sig generation since it is also the value used in timestamp parameter val messageTimestamp = message.timestamp - val userEd25519KeyPair = try { - MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null + val userED25519KeyPair = runCatching { MessagingModuleConfiguration.shared.getUserED25519KeyPair() }.getOrNull() ?: return null + + val verificationData = "store$namespace$messageTimestamp".toByteArray() + val signature = try { + signAndEncode(verificationData, userED25519KeyPair) } catch (e: Exception) { + Log.e("Loki", "Signing data failed with user secret key", e) return null } - val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString - val signature = ByteArray(Sign.BYTES) - val verificationData = "store$namespace$messageTimestamp".toByteArray() - try { - sodium.cryptoSignDetached( - signature, - verificationData, - verificationData.size.toLong(), - userEd25519KeyPair.secretKey.asBytes - ) - } catch (e: Exception) { - Log.e("Loki", "Signing data failed with user secret key", e) + val params = buildMap { + // load the message data params into the sub request + // currently loads: + // pubKey + // data + // ttl + // timestamp + putAll(message.toJSON()) + this["namespace"] = namespace + // timestamp already set + this["pubkey_ed25519"] = userED25519KeyPair.publicKey.asHexString + this["signature"] = signature } - // timestamp already set - params["pubkey_ed25519"] = ed25519PublicKey - params["signature"] = Base64.encodeBytes(signature) + return SnodeBatchRequestInfo( Snode.Method.SendMessage.rawValue, params, @@ -444,32 +376,26 @@ object SnodeAPI { * @param required indicates that *at least one* message in the list is deleted from the server, otherwise it will return 404 */ fun buildAuthenticatedDeleteBatchInfo(publicKey: String, messageHashes: List, required: Boolean = false): SnodeBatchRequestInfo? { - val params = mutableMapOf( - "pubkey" to publicKey, - "required" to required, // could be omitted technically but explicit here - "messages" to messageHashes - ) val userEd25519KeyPair = try { MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null } catch (e: Exception) { return null } val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString - val signature = ByteArray(Sign.BYTES) - val verificationData = "delete${messageHashes.joinToString("")}".toByteArray() - try { - sodium.cryptoSignDetached( - signature, - verificationData, - verificationData.size.toLong(), - userEd25519KeyPair.secretKey.asBytes - ) + val verificationData = sequenceOf("delete").plus(messageHashes).toByteArray() + val signature = try { + signAndEncode(verificationData, userEd25519KeyPair) } catch (e: Exception) { Log.e("Loki", "Signing data failed with user secret key", e) return null } - params["pubkey_ed25519"] = ed25519PublicKey - params["signature"] = Base64.encodeBytes(signature) + val params = buildMap { + this["pubkey"] = publicKey + this["required"] = required // could be omitted technically but explicit here + this["messages"] = messageHashes + this["pubkey_ed25519"] = ed25519PublicKey + this["signature"] = signature + } return SnodeBatchRequestInfo( Snode.Method.DeleteMessage.rawValue, params, @@ -479,39 +405,25 @@ object SnodeAPI { fun buildAuthenticatedRetrieveBatchRequest(snode: Snode, publicKey: String, namespace: Int = 0, maxSize: Int? = null): SnodeBatchRequestInfo? { val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: "" - val params = mutableMapOf( - "pubkey" to publicKey, - "last_hash" to lastHashValue, - ) - val userEd25519KeyPair = try { - MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null - } catch (e: Exception) { - return null - } + val userEd25519KeyPair = runCatching { MessagingModuleConfiguration.shared.getUserED25519KeyPair() }.getOrNull() ?: return null val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString val timestamp = System.currentTimeMillis() + clockOffset - val signature = ByteArray(Sign.BYTES) val verificationData = if (namespace == 0) "retrieve$timestamp".toByteArray() else "retrieve$namespace$timestamp".toByteArray() - try { - sodium.cryptoSignDetached( - signature, - verificationData, - verificationData.size.toLong(), - userEd25519KeyPair.secretKey.asBytes - ) + val signature = try { + signAndEncode(verificationData, userEd25519KeyPair) } catch (e: Exception) { Log.e("Loki", "Signing data failed with user secret key", e) return null } - params["timestamp"] = timestamp - params["pubkey_ed25519"] = ed25519PublicKey - params["signature"] = Base64.encodeBytes(signature) - if (namespace != 0) { - params["namespace"] = namespace - } - if (maxSize != null) { - params["max_size"] = maxSize + val params = buildMap { + this["pubkey"] = publicKey + this["last_hash"] = lastHashValue + this["timestamp"] = timestamp + this["pubkey_ed25519"] = ed25519PublicKey + this["signature"] = signature + if (namespace != 0) this["namespace"] = namespace + if (maxSize != null) this["max_size"] = maxSize } return SnodeBatchRequestInfo( Snode.Method.Retrieve.rawValue, @@ -535,13 +447,12 @@ object SnodeAPI { } fun getRawBatchResponse(snode: Snode, publicKey: String, requests: List, sequence: Boolean = false): RawResponsePromise { - val parameters = mutableMapOf( - "requests" to requests - ) + val parameters = buildMap { this["requests"] = requests } return invoke(if (sequence) Snode.Method.Sequence else Snode.Method.Batch, snode, parameters, publicKey).success { rawResponses -> - val responseList = (rawResponses["results"] as List) - responseList.forEachIndexed { index, response -> - if (response["code"] as? Int != 200) { + rawResponses["results"].let { it as List } + .asSequence() + .filter { it["code"] as? Int != 200 } + .forEach { response -> Log.w("Loki", "response code was not 200") handleSnodeError( response["code"] as? Int ?: 0, @@ -550,7 +461,6 @@ object SnodeAPI { publicKey ) } - } } } @@ -562,14 +472,8 @@ object SnodeAPI { val signData = "${Snode.Method.GetExpiries.rawValue}$timestamp${hashes.joinToString(separator = "")}".toByteArray() val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString - val signature = ByteArray(Sign.BYTES) - try { - sodium.cryptoSignDetached( - signature, - signData, - signData.size.toLong(), - userEd25519KeyPair.secretKey.asBytes - ) + val signature = try { + signAndEncode(signData, userEd25519KeyPair) } catch (e: Exception) { Log.e("Loki", "Signing data failed with user secret key", e) return@retryIfNeeded Promise.ofFail(e) @@ -579,7 +483,7 @@ object SnodeAPI { "messages" to hashes, "timestamp" to timestamp, "pubkey_ed25519" to ed25519PublicKey, - "signature" to Base64.encodeBytes(signature) + "signature" to signature ) getSingleTargetSnode(publicKey) bind { snode -> invoke(Snode.Method.GetExpiries, snode, params, publicKey) @@ -587,8 +491,8 @@ object SnodeAPI { } } - fun alterTtl(messageHashes: List, newExpiry: Long, publicKey: String, extend: Boolean = false, shorten: Boolean = false): RawResponsePromise { - return retryIfNeeded(maxRetryCount) { + fun alterTtl(messageHashes: List, newExpiry: Long, publicKey: String, extend: Boolean = false, shorten: Boolean = false): RawResponsePromise = + retryIfNeeded(maxRetryCount) { val params = buildAlterTtlParams(messageHashes, newExpiry, publicKey, extend, shorten) ?: return@retryIfNeeded Promise.ofFail( Exception("Couldn't build signed params for alterTtl request for newExpiry=$newExpiry, extend=$extend, shorten=$shorten") @@ -597,111 +501,96 @@ object SnodeAPI { invoke(Snode.Method.Expire, snode, params, publicKey) } } - } private fun buildAlterTtlParams( // TODO: in future this will probably need to use the closed group subkeys / admin keys for group swarms messageHashes: List, newExpiry: Long, publicKey: String, extend: Boolean = false, - shorten: Boolean = false): Map? { + shorten: Boolean = false + ): Map? { val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null - val params = mutableMapOf( - "expiry" to newExpiry, - "messages" to messageHashes, - ) - if (extend) { - params["extend"] = true - } else if (shorten) { - params["shorten"] = true - } + val shortenOrExtend = if (extend) "extend" else if (shorten) "shorten" else "" val signData = "${Snode.Method.Expire.rawValue}$shortenOrExtend$newExpiry${messageHashes.joinToString(separator = "")}".toByteArray() - val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString - val signature = ByteArray(Sign.BYTES) - try { - sodium.cryptoSignDetached( - signature, - signData, - signData.size.toLong(), - userEd25519KeyPair.secretKey.asBytes - ) + val signature = try { + signAndEncode(signData, userEd25519KeyPair) } catch (e: Exception) { Log.e("Loki", "Signing data failed with user secret key", e) return null } - params["pubkey"] = publicKey - params["pubkey_ed25519"] = ed25519PublicKey - params["signature"] = Base64.encodeBytes(signature) - return params - } - - fun getMessages(publicKey: String): MessageListPromise { - return retryIfNeeded(maxRetryCount) { - getSingleTargetSnode(publicKey).bind { snode -> - getRawMessages(snode, publicKey).map { parseRawMessagesResponse(it, snode, publicKey) } + return buildMap { + this["expiry"] = newExpiry + this["messages"] = messageHashes + when { + extend -> this["extend"] = true + shorten -> this["shorten"] = true } + this["pubkey"] = publicKey + this["pubkey_ed25519"] = userEd25519KeyPair.publicKey.asHexString + this["signature"] = signature } } - private fun getNetworkTime(snode: Snode): Promise, Exception> { - return invoke(Snode.Method.Info, snode, emptyMap()).map { rawResponse -> + fun getMessages(publicKey: String): MessageListPromise = retryIfNeeded(maxRetryCount) { + getSingleTargetSnode(publicKey).bind { snode -> + getRawMessages(snode, publicKey).map { parseRawMessagesResponse(it, snode, publicKey) } + } + } + + private fun getNetworkTime(snode: Snode): Promise, Exception> = + invoke(Snode.Method.Info, snode, emptyMap()).map { rawResponse -> val timestamp = rawResponse["timestamp"] as? Long ?: -1 snode to timestamp } - } - fun sendMessage(message: SnodeMessage, requiresAuth: Boolean = false, namespace: Int = 0): RawResponsePromise { - val destination = message.recipient - return retryIfNeeded(maxRetryCount) { + fun sendMessage(message: SnodeMessage, requiresAuth: Boolean = false, namespace: Int = 0): RawResponsePromise = + retryIfNeeded(maxRetryCount) { val module = MessagingModuleConfiguration.shared val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) - val parameters = message.toJSON().toMutableMap() + val parameters = message.toJSON().toMutableMap() // Construct signature if (requiresAuth) { val sigTimestamp = nowWithOffset val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString - val signature = ByteArray(Sign.BYTES) // assume namespace here is non-zero, as zero namespace doesn't require auth val verificationData = "store$namespace$sigTimestamp".toByteArray() - try { - sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes) + val signature = try { + signAndEncode(verificationData, userED25519KeyPair) } catch (exception: Exception) { return@retryIfNeeded Promise.ofFail(Error.SigningFailed) } parameters["sig_timestamp"] = sigTimestamp parameters["pubkey_ed25519"] = ed25519PublicKey - parameters["signature"] = Base64.encodeBytes(signature) + parameters["signature"] = signature } // If the namespace is default (0) here it will be implicitly read as 0 on the storage server // we only need to specify it explicitly if we want to (in future) or if it is non-zero if (namespace != 0) { parameters["namespace"] = namespace } + val destination = message.recipient getSingleTargetSnode(destination).bind { snode -> invoke(Snode.Method.SendMessage, snode, parameters, destination) } } - } - fun deleteMessage(publicKey: String, serverHashes: List): Promise, Exception> { - return retryIfNeeded(maxRetryCount) { + fun deleteMessage(publicKey: String, serverHashes: List): Promise, Exception> = + retryIfNeeded(maxRetryCount) { val module = MessagingModuleConfiguration.shared val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) getSingleTargetSnode(publicKey).bind { snode -> retryIfNeeded(maxRetryCount) { - val signature = ByteArray(Sign.BYTES) val verificationData = sequenceOf(Snode.Method.DeleteMessage.rawValue).plus(serverHashes).toByteArray() - sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes) val deleteMessageParams = mapOf( "pubkey" to userPublicKey, "pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString, "messages" to serverHashes, - "signature" to Base64.encodeBytes(signature) + "signature" to signAndEncode(verificationData, userED25519KeyPair) ) invoke(Snode.Method.DeleteMessage, snode, deleteMessageParams, publicKey).map { rawResponse -> val swarms = rawResponse["swarm"] as? Map ?: return@map mapOf() @@ -729,44 +618,36 @@ object SnodeAPI { } } } - } // Parsing private fun parseSnodes(rawResponse: Any): List = (rawResponse as? Map<*, *>) ?.run { get("snodes") as? List<*> } - ?.mapNotNull { rawSnode -> - val rawSnodeAsJSON = rawSnode as? Map<*, *> - val address = rawSnodeAsJSON?.get("ip") as? String - val portAsString = rawSnodeAsJSON?.get("port") as? String - val port = portAsString?.toInt() - val ed25519Key = rawSnodeAsJSON?.get(KEY_ED25519) as? String - val x25519Key = rawSnodeAsJSON?.get(KEY_X25519) as? String + ?.asSequence() + ?.mapNotNull { it as? Map<*, *> } + ?.mapNotNull { + createSnode( + address = it["ip"] as? String, + port = (it["port"] as? String)?.toInt(), + ed25519Key = it[KEY_ED25519] as? String, + x25519Key = it[KEY_X25519] as? String + ).apply { if (this == null) Log.d("Loki", "Failed to parse snode from: ${it.prettifiedDescription()}.") } + }?.toList() ?: listOf().also { Log.d("Loki", "Failed to parse snodes from: ${rawResponse.prettifiedDescription()}.") } - if (address != null && port != null && ed25519Key != null && x25519Key != null && address != "0.0.0.0") { - Snode("https://$address", port, Snode.KeySet(ed25519Key, x25519Key), Snode.Version.ZERO) - } else { - Log.d("Loki", "Failed to parse snode from: ${rawSnode?.prettifiedDescription()}.") - null - } - } ?: listOf().also { Log.d("Loki", "Failed to parse snodes from: ${rawResponse.prettifiedDescription()}.") } - - fun deleteAllMessages(): Promise, Exception> { - return retryIfNeeded(maxRetryCount) { + fun deleteAllMessages(): Promise, Exception> = + retryIfNeeded(maxRetryCount) { val module = MessagingModuleConfiguration.shared val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) getSingleTargetSnode(userPublicKey).bind { snode -> retryIfNeeded(maxRetryCount) { getNetworkTime(snode).bind { (_, timestamp) -> - val signature = ByteArray(Sign.BYTES) val verificationData = (Snode.Method.DeleteAll.rawValue + Namespace.ALL + timestamp.toString()).toByteArray() - sodium.cryptoSignDetached(signature, verificationData, verificationData.size.toLong(), userED25519KeyPair.secretKey.asBytes) val deleteMessageParams = mapOf( "pubkey" to userPublicKey, "pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString, "timestamp" to timestamp, - "signature" to Base64.encodeBytes(signature), + "signature" to signAndEncode(verificationData, userED25519KeyPair), "namespace" to Namespace.ALL, ) invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey).map { @@ -778,75 +659,14 @@ object SnodeAPI { } } } - } - fun updateExpiry(updatedExpiryMs: Long, serverHashes: List): Promise, Long>>, Exception> { - return retryIfNeeded(maxRetryCount) { - val module = MessagingModuleConfiguration.shared - val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) - val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) - val updatedExpiryMsWithNetworkOffset = updatedExpiryMs + clockOffset - getSingleTargetSnode(userPublicKey).bind { snode -> - retryIfNeeded(maxRetryCount) { - // "expire" || expiry || messages[0] || ... || messages[N] - val verificationData = sequenceOf(Snode.Method.Expire.rawValue, "$updatedExpiryMsWithNetworkOffset").plus(serverHashes).toByteArray() - val signature = ByteArray(Sign.BYTES) - sodium.cryptoSignDetached( - signature, - verificationData, - verificationData.size.toLong(), - userED25519KeyPair.secretKey.asBytes - ) - val params = mapOf( - "pubkey" to userPublicKey, - "pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString, - "expiry" to updatedExpiryMs, - "messages" to serverHashes, - "signature" to Base64.encodeBytes(signature) - ) - invoke(Snode.Method.Expire, snode, params, userPublicKey).map { rawResponse -> - val swarms = rawResponse["swarm"] as? Map ?: return@map mapOf() - val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) -> - val json = rawJSON as? Map ?: return@mapNotNull null - val isFailed = json["failed"] as? Boolean ?: false - val statusCode = json["code"] as? String - val reason = json["reason"] as? String - hexSnodePublicKey to if (isFailed) { - Log.e("Loki", "Failed to update expiry for: $hexSnodePublicKey due to error: $reason ($statusCode).") - listOf() to 0L - } else { - val hashes = json["updated"] as List - val expiryApplied = json["expiry"] as Long - val signature = json["signature"] as String - val snodePublicKey = Key.fromHexString(hexSnodePublicKey) - // The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] ) - val message = sequenceOf(userPublicKey).plus(serverHashes).plus(hashes).toByteArray() - if (sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes)) { - hashes to expiryApplied - } else listOf() to 0L - } - } - return@map result.toMap() - }.fail { e -> - Log.e("Loki", "Failed to update expiry", e) - } - } - } - } - } - - fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true, updateStoredHashes: Boolean = true): List> { - val messages = rawResponse["messages"] as? List<*> - return if (messages != null) { + fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true, updateStoredHashes: Boolean = true): List> = + (rawResponse["messages"] as? List<*>)?.let { messages -> if (updateLatestHash) { updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace) } - val newRawMessages = removeDuplicates(publicKey, messages, namespace, updateStoredHashes) - return parseEnvelopes(newRawMessages) - } else { - listOf() - } - } + removeDuplicates(publicKey, messages, namespace, updateStoredHashes).let(::parseEnvelopes) + } ?: listOf() fun updateLastMessageHashValueIfPossible(snode: Snode, publicKey: String, rawMessages: List<*>, namespace: Int) { val lastMessageAsJSON = rawMessages.lastOrNull() as? Map<*, *> @@ -859,34 +679,33 @@ object SnodeAPI { } fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int, updateStoredHashes: Boolean): List<*> { - val originalMessageHashValues = database.getReceivedMessageHashValues(publicKey, namespace)?.toMutableSet() ?: mutableSetOf() + val originalMessageHashValues = database.getReceivedMessageHashValues(publicKey, namespace) ?: emptySet() val receivedMessageHashValues = originalMessageHashValues.toMutableSet() val result = rawMessages.filter { rawMessage -> - val rawMessageAsJSON = rawMessage as? Map<*, *> - val hashValue = rawMessageAsJSON?.get("hash") as? String - if (hashValue != null) { - val isDuplicate = receivedMessageHashValues.contains(hashValue) - receivedMessageHashValues.add(hashValue) - !isDuplicate - } else { - Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.") - false - } + (rawMessage as? Map<*, *>) + ?.let { it["hash"] as? String } + ?.let { receivedMessageHashValues.add(it) } + ?: false.also { Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.") } } - if (originalMessageHashValues != receivedMessageHashValues && updateStoredHashes) { + if (updateStoredHashes && originalMessageHashValues.containsAll(receivedMessageHashValues)) { database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues, namespace) } return result } - private fun parseEnvelopes(rawMessages: List<*>): List> { - return rawMessages.mapNotNull { rawMessage -> + private fun parseEnvelopes(rawMessages: List<*>): List> = + rawMessages.mapNotNull { rawMessage -> val rawMessageAsJSON = rawMessage as? Map<*, *> val base64EncodedData = rawMessageAsJSON?.get("data") as? String val data = base64EncodedData?.let { Base64.decode(it) } + + data?.runCatching(MessageWrapper::unwrap) + ?.map { it to rawMessageAsJSON["hash"] as? String } + ?.onFailure { Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") } + if (data != null) { try { - Pair(MessageWrapper.unwrap(data), rawMessageAsJSON.get("hash") as? String) + MessageWrapper.unwrap(data) to rawMessageAsJSON["hash"] as? String } catch (e: Exception) { Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") null @@ -896,12 +715,11 @@ object SnodeAPI { null } } - } @Suppress("UNCHECKED_CAST") private fun parseDeletions(userPublicKey: String, timestamp: Long, rawResponse: RawResponse): Map { val swarms = rawResponse["swarm"] as? Map ?: return mapOf() - val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) -> + return swarms.mapNotNull { (hexSnodePublicKey, rawJSON) -> val json = rawJSON as? Map ?: return@mapNotNull null val isFailed = json["failed"] as? Boolean ?: false val statusCode = json["code"] as? String @@ -917,14 +735,13 @@ object SnodeAPI { val message = sequenceOf(userPublicKey, "$timestamp").plus(hashes).toByteArray() sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes) } - } - return result.toMap() + }.toMap() } // endregion // Error Handling - internal fun handleSnodeError(statusCode: Int, json: Map<*, *>?, snode: Snode, publicKey: String? = null): Exception? { + internal fun handleSnodeError(statusCode: Int, json: Map<*, *>?, snode: Snode, publicKey: String? = null): Throwable? = runCatching { fun handleBadSnode() { val oldFailureCount = snodeFailureCount[snode] ?: 0 val newFailureCount = oldFailureCount + 1 @@ -932,56 +749,43 @@ object SnodeAPI { Log.d("Loki", "Couldn't reach snode at $snode; setting failure count to $newFailureCount.") if (newFailureCount >= snodeFailureThreshold) { Log.d("Loki", "Failure threshold reached for: $snode; dropping it.") - if (publicKey != null) { - dropSnodeFromSwarmIfNeeded(snode, publicKey) - } - snodePool = snodePool.toMutableSet().minus(snode).toSet() + publicKey?.let { dropSnodeFromSwarmIfNeeded(snode, it) } + snodePool -= snode Log.d("Loki", "Snode pool count: ${snodePool.count()}.") snodeFailureCount[snode] = 0 } } when (statusCode) { - 400, 500, 502, 503 -> { // Usually indicates that the snode isn't up to date - handleBadSnode() - } + // Usually indicates that the snode isn't up to date + 400, 500, 502, 503 -> handleBadSnode() 406 -> { Log.d("Loki", "The user's clock is out of sync with the service node network.") broadcaster.broadcast("clockOutOfSync") - return Error.ClockOutOfSync + throw Error.ClockOutOfSync } 421 -> { // The snode isn't associated with the given public key anymore if (publicKey != null) { - fun invalidateSwarm() { - Log.d("Loki", "Invalidating swarm for: $publicKey.") - dropSnodeFromSwarmIfNeeded(snode, publicKey) - } - if (json != null) { - val snodes = parseSnodes(json) - if (snodes.isNotEmpty()) { - database.setSwarm(publicKey, snodes.toSet()) - } else { - invalidateSwarm() + json?.let(::parseSnodes) + ?.takeIf { it.isNotEmpty() } + ?.let { database.setSwarm(publicKey, it.toSet()) } + ?: run { + Log.d("Loki", "Invalidating swarm for: $publicKey.") + dropSnodeFromSwarmIfNeeded(snode, publicKey) } - } else { - invalidateSwarm() - } - } else { - Log.d("Loki", "Got a 421 without an associated public key.") - } + } else Log.d("Loki", "Got a 421 without an associated public key.") } 404 -> { Log.d("Loki", "404, probably no file found") - return Error.Generic + throw Error.Generic } else -> { handleBadSnode() Log.d("Loki", "Unhandled response code: ${statusCode}.") - return Error.Generic + throw Error.Generic } } - return null - } + }.exceptionOrNull() } // Type Aliases diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/Base64.java b/libsignal/src/main/java/org/session/libsignal/utilities/Base64.java index 3ff38f76d3..35ec22e0e0 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Base64.java +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Base64.java @@ -1,5 +1,7 @@ package org.session.libsignal.utilities; +import androidx.annotation.NonNull; + /** *

Encodes and decodes to and from Base64 notation.

*

Homepage: http://iharder.net/base64.

@@ -714,7 +716,7 @@ public class Base64 * @throws NullPointerException if source array is null * @since 1.4 */ - public static String encodeBytes( byte[] source ) { + public static String encodeBytes(@NonNull byte[] source ) { // Since we're not going to have the GZIP encoding turned on, // we're not going to have an java.io.IOException thrown, so // we should not force the user to have to catch it. From c1d40cdbe73605c02a4718d0600cb419177cd5b5 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Sat, 3 Aug 2024 20:19:29 +0930 Subject: [PATCH 35/45] Optimise SnodesAPI --- .../org/session/libsession/snode/SnodeAPI.kt | 349 ++++++++---------- .../org/session/libsession/utilities/Util.kt | 21 ++ 2 files changed, 181 insertions(+), 189 deletions(-) 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 8e19234b0d..6d1ffaa5c0 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -18,6 +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.libsession.utilities.buildMutableMap +import org.session.libsession.utilities.mapValuesNotNull import org.session.libsession.utilities.toByteArray import org.session.libsignal.crypto.getRandomElement import org.session.libsignal.database.LokiAPIDatabaseProtocol @@ -73,20 +75,18 @@ object SnodeAPI { // Use port 4433 if the API level can handle the network security configuration and enforce pinned certificates private val seedNodePort = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 443 else 4443 - private const val snodeFailureThreshold = 3 - private const val useOnionRequests = true - private const val useTestnet = false - private val seedNodePool = if (useTestnet) { - setOf( "http://public.loki.foundation:38157" ) - } else { - setOf( - "https://seed1.getsession.org:$seedNodePort", - "https://seed2.getsession.org:$seedNodePort", - "https://seed3.getsession.org:$seedNodePort", - ) - } + private val seedNodePool = if (useTestnet) setOf( + "http://public.loki.foundation:38157" + ) else setOf( + "https://seed1.getsession.org:$seedNodePort", + "https://seed2.getsession.org:$seedNodePort", + "https://seed3.getsession.org:$seedNodePort", + ) + + private const val snodeFailureThreshold = 3 + private const val useOnionRequests = true private const val KEY_IP = "public_ip" private const val KEY_PORT = "storage_port" @@ -121,48 +121,45 @@ object SnodeAPI { parameters: Map, publicKey: String? = null, version: Version = Version.V3 - ): RawResponsePromise = if (useOnionRequests) OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey).map { - val body = it.body ?: throw Error.Generic - JsonUtil.fromJson(body, Map::class.java) - } else task { - val payload = mapOf( "method" to method.rawValue, "params" to parameters ) - try { - val url = "${snode.address}:${snode.port}/storage_rpc/v1" - val response = HTTP.execute(HTTP.Verb.POST, url, payload).toString() - JsonUtil.fromJson(response, Map::class.java) - } catch (exception: Exception) { - (exception as? HTTP.HTTPRequestFailedException)?.run { - handleSnodeError(statusCode, json, snode, publicKey) - // TODO Check if we meant to throw the error returned by handleSnodeError - throw exception + ): RawResponsePromise = when { + useOnionRequests -> OnionRequestAPI.sendOnionRequest(method, parameters, snode, version, publicKey).map { + JsonUtil.fromJson(it.body ?: throw Error.Generic, Map::class.java) + } + else -> task { + HTTP.execute( + HTTP.Verb.POST, + url = "${snode.address}:${snode.port}/storage_rpc/v1", + parameters = buildMap { + this["method"] = method.rawValue + this["params"] = parameters } - Log.d("Loki", "Unhandled exception: $exception.") - throw exception + ).toString().let { + JsonUtil.fromJson(it, Map::class.java) + } + }.fail { e -> + when (e) { + is HTTP.HTTPRequestFailedException -> handleSnodeError(e.statusCode, e.json, snode, publicKey) + else -> Log.d("Loki", "Unhandled exception: $e.") } } + } + + private val GET_RANDOM_SNODE_PARAMS = buildMap { + this["method"] = "get_n_service_nodes" + this["params"] = buildMap { + this["active_only"] = true + this["fields"] = sequenceOf(KEY_IP, KEY_PORT, KEY_X25519, KEY_ED25519, KEY_VERSION).associateWith { true } + } + } internal fun getRandomSnode(): Promise = - snodePool.takeIf { it.size >= minimumSnodePoolCount }?.let { Promise.of(it.getRandomElement()) } ?: task { + snodePool.takeIf { it.size >= minimumSnodePoolCount }?.getRandomElement()?.let { Promise.of(it) } ?: task { val target = seedNodePool.random() - val url = "$target/json_rpc" Log.d("Loki", "Populating snode pool using: $target.") - val parameters = mapOf( - "method" to "get_n_service_nodes", - "params" to mapOf( - "active_only" 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 response = HTTP.execute(HTTP.Verb.POST, url, parameters, useSeedNodeConnection = true) - val json = try { - JsonUtil.fromJson(response, Map::class.java) - } catch (exception: Exception) { - mapOf( "result" to response.toString()) - } + val url = "$target/json_rpc" + val response = HTTP.execute(HTTP.Verb.POST, url, GET_RANDOM_SNODE_PARAMS, useSeedNodeConnection = true) + val json = runCatching { JsonUtil.fromJson(response, Map::class.java) }.getOrNull() + ?: buildMap { this["result"] = response.toString() } val intermediate = json["result"] as? Map<*, *> ?: throw Error.Generic .also { Log.d("Loki", "Failed to update snode pool, intermediate was null.") } val rawSnodes = intermediate["service_node_states"] as? List<*> ?: throw Error.Generic @@ -180,7 +177,7 @@ object SnodeAPI { ).also { if (it == null) Log.d("Loki", "Failed to parse: ${rawSnode.prettifiedDescription()}.") } }.toSet().also { Log.d("Loki", "Persisting snode pool to database.") - this.snodePool = it + snodePool = it }.runCatching { getRandomElement() }.onFailure { Log.d("Loki", "Got an empty snode pool from: $target.") throw SnodeAPI.Error.Generic @@ -220,10 +217,13 @@ object SnodeAPI { } val base64EncodedNameHash = Base64.encodeBytes(nameHash) // Ask 3 different snodes for the Account ID associated with the given name hash - val parameters = mapOf( - "endpoint" to "ons_resolve", - "params" to mapOf( "type" to 0, "name_hash" to base64EncodedNameHash ) - ) + val parameters = buildMap { + this["endpoint"] = "ons_resolve" + this["params"] = buildMap { + this["type"] = 0 + this["name_hash"] = base64EncodedNameHash + } + } val promises = List(validationCount) { getRandomSnode().bind { snode -> retryIfNeeded(maxRetryCount) { @@ -232,10 +232,9 @@ object SnodeAPI { } } return all(promises).map { results -> - val accountIDs = mutableListOf() - for (json in results) { - val intermediate = json["result"] as? Map<*, *> - val hexEncodedCiphertext = intermediate?.get("encrypted_value") as? String ?: throw Error.Generic + results.map { json -> + val intermediate = json["result"] as? Map<*, *> ?: throw Error.Generic + val hexEncodedCiphertext = intermediate["encrypted_value"] as? String ?: throw Error.Generic val ciphertext = Hex.fromStringCondensed(hexEncodedCiphertext) val isArgon2Based = (intermediate["nonce"] == null) if (isArgon2Based) { @@ -251,7 +250,7 @@ object SnodeAPI { if (!sodium.cryptoSecretBoxOpenEasy(accountIDAsData, ciphertext, ciphertext.size.toLong(), nonce, key)) { throw Error.DecryptionFailed } - accountIDs.add(Hex.toStringCondensed(accountIDAsData)) + Hex.toStringCondensed(accountIDAsData) } else { val hexEncodedNonce = intermediate["nonce"] as? String ?: throw Error.Generic val nonce = Hex.fromStringCondensed(hexEncodedNonce) @@ -263,10 +262,9 @@ object SnodeAPI { if (!sodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(accountIDAsData, null, null, ciphertext, ciphertext.size.toLong(), null, 0, nonce, key)) { throw Error.DecryptionFailed } - accountIDs.add(Hex.toStringCondensed(accountIDAsData)) + Hex.toStringCondensed(accountIDAsData) } - } - accountIDs.takeIf { it.size == validationCount && it.toSet().size == 1 }?.first() + }.takeIf { it.size == validationCount && it.toSet().size == 1 }?.first() ?: throw Error.ValidationFailed } } @@ -274,30 +272,31 @@ object SnodeAPI { fun getSwarm(publicKey: String): Promise, Exception> = database.getSwarm(publicKey)?.takeIf { it.size >= minimumSwarmSnodeCount }?.let(Promise.Companion::of) ?: getRandomSnode().bind { - invoke(Snode.Method.GetSwarm, it, parameters = mapOf( "pubKey" to publicKey ), publicKey) + invoke(Snode.Method.GetSwarm, it, parameters = buildMap { this["pubKey"] = publicKey }, publicKey) }.map { parseSnodes(it).toSet() }.success { database.setSwarm(publicKey, it) } - private fun signAndEncode(data: ByteArray, userED25519KeyPair: KeyPair) = sign(data, userED25519KeyPair).let(Base64::encodeBytes) + private fun signAndEncodeCatching(data: ByteArray, userED25519KeyPair: KeyPair): Result = + runCatching { signAndEncode(data, userED25519KeyPair) } + private fun signAndEncode(data: ByteArray, userED25519KeyPair: KeyPair): String = + sign(data, userED25519KeyPair).let(Base64::encodeBytes) private fun sign(data: ByteArray, userED25519KeyPair: KeyPair): ByteArray = ByteArray(Sign.BYTES).also { - sodium.cryptoSignDetached( - it, - data, - data.size.toLong(), - userED25519KeyPair.secretKey.asBytes - ) + sodium.cryptoSignDetached(it, data, data.size.toLong(), userED25519KeyPair.secretKey.asBytes) } fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise { // Get last message hash val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: "" - val parameters = mutableMapOf( - "pubKey" to publicKey, - "last_hash" to lastHashValue, - ) + val parameters = buildMutableMap { + this["pubKey"] = publicKey + this["last_hash"] = lastHashValue + // If the namespace is default (0) here it will be implicitly read as 0 on the storage server + // we only need to specify it explicitly if we want to (in future) or if it is non-zero + namespace.takeIf { it != 0 }?.let { this["namespace"] = it } + } // Construct signature if (requiresAuth) { val userED25519KeyPair = try { @@ -311,23 +310,13 @@ object SnodeAPI { val ed25519PublicKey = userED25519KeyPair.publicKey.asHexString val verificationData = buildString { append("retrieve") - if (namespace != 0) append(namespace) + namespace.takeIf { it != 0 }?.let(::append) append(timestamp) }.toByteArray() - val signature = try { - signAndEncode(verificationData, userED25519KeyPair) - } catch (exception: Exception) { - return Promise.ofFail(Error.SigningFailed) - } + parameters["signature"] = signAndEncodeCatching(verificationData, userED25519KeyPair).getOrNull() + ?: return Promise.ofFail(Error.SigningFailed) parameters["timestamp"] = timestamp parameters["pubkey_ed25519"] = ed25519PublicKey - parameters["signature"] = signature - } - - // If the namespace is default (0) here it will be implicitly read as 0 on the storage server - // we only need to specify it explicitly if we want to (in future) or if it is non-zero - if (namespace != 0) { - parameters["namespace"] = namespace } // Make the request @@ -341,11 +330,8 @@ object SnodeAPI { val userED25519KeyPair = runCatching { MessagingModuleConfiguration.shared.getUserED25519KeyPair() }.getOrNull() ?: return null val verificationData = "store$namespace$messageTimestamp".toByteArray() - val signature = try { - signAndEncode(verificationData, userED25519KeyPair) - } catch (e: Exception) { - Log.e("Loki", "Signing data failed with user secret key", e) - return null + val signature = signAndEncodeCatching(verificationData, userED25519KeyPair).run { + getOrNull() ?: return null.also { Log.e("Loki", "Signing data failed with user secret key", exceptionOrNull()) } } val params = buildMap { @@ -478,13 +464,13 @@ object SnodeAPI { Log.e("Loki", "Signing data failed with user secret key", e) return@retryIfNeeded Promise.ofFail(e) } - val params = mapOf( - "pubkey" to publicKey, - "messages" to hashes, - "timestamp" to timestamp, - "pubkey_ed25519" to ed25519PublicKey, - "signature" to signature - ) + val params = buildMap { + this["pubkey"] = publicKey + this["messages"] = hashes + this["timestamp"] = timestamp + this["pubkey_ed25519"] = ed25519PublicKey + this["signature"] = signature + } getSingleTargetSnode(publicKey) bind { snode -> invoke(Snode.Method.GetExpiries, snode, params, publicKey) } @@ -578,7 +564,7 @@ object SnodeAPI { } } - fun deleteMessage(publicKey: String, serverHashes: List): Promise, Exception> = + fun deleteMessage(publicKey: String, serverHashes: List): Promise, Exception> = retryIfNeeded(maxRetryCount) { val module = MessagingModuleConfiguration.shared val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) @@ -586,35 +572,40 @@ object SnodeAPI { getSingleTargetSnode(publicKey).bind { snode -> retryIfNeeded(maxRetryCount) { val verificationData = sequenceOf(Snode.Method.DeleteMessage.rawValue).plus(serverHashes).toByteArray() - val deleteMessageParams = mapOf( - "pubkey" to userPublicKey, - "pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString, - "messages" to serverHashes, - "signature" to signAndEncode(verificationData, userED25519KeyPair) - ) + val deleteMessageParams = buildMap { + this["pubkey"] = userPublicKey + this["pubkey_ed25519"] = userED25519KeyPair.publicKey.asHexString + this["messages"] = serverHashes + this["signature"] = signAndEncode(verificationData, userED25519KeyPair) + } invoke(Snode.Method.DeleteMessage, snode, deleteMessageParams, publicKey).map { rawResponse -> val swarms = rawResponse["swarm"] as? Map ?: return@map mapOf() - val result = swarms.mapNotNull { (hexSnodePublicKey, rawJSON) -> - val json = rawJSON as? Map ?: return@mapNotNull null - val isFailed = json["failed"] as? Boolean ?: false - val statusCode = json["code"] as? String - val reason = json["reason"] as? String - hexSnodePublicKey to if (isFailed) { - Log.e("Loki", "Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode).") - false - } else { - val hashes = json["deleted"] as List // Hashes of deleted messages - val signature = json["signature"] as String - val snodePublicKey = Key.fromHexString(hexSnodePublicKey) - // The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] ) - val message = sequenceOf(userPublicKey).plus(serverHashes).plus(hashes).toByteArray() - sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes) + swarms.mapValuesNotNull { (hexSnodePublicKey, rawJSON) -> + (rawJSON as? Map)?.let { json -> + val isFailed = json["failed"] as? Boolean ?: false + val statusCode = json["code"] as? String + val reason = json["reason"] as? String + + if (isFailed) { + Log.e("Loki", "Failed to delete messages from: $hexSnodePublicKey due to error: $reason ($statusCode).") + false + } else { + // Hashes of deleted messages + val hashes = json["deleted"] as List + val signature = json["signature"] as String + val snodePublicKey = Key.fromHexString(hexSnodePublicKey) + // The signature looks like ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] ) + val message = sequenceOf(userPublicKey).plus(serverHashes).plus(hashes).toByteArray() + sodium.cryptoSignVerifyDetached( + Base64.decode(signature), + message, + message.size, + snodePublicKey.asBytes + ) + } } } - return@map result.toMap() - }.fail { e -> - Log.e("Loki", "Failed to delete messages", e) - } + }.fail { e -> Log.e("Loki", "Failed to delete messages", e) } } } } @@ -643,18 +634,16 @@ object SnodeAPI { retryIfNeeded(maxRetryCount) { getNetworkTime(snode).bind { (_, timestamp) -> val verificationData = (Snode.Method.DeleteAll.rawValue + Namespace.ALL + timestamp.toString()).toByteArray() - val deleteMessageParams = mapOf( - "pubkey" to userPublicKey, - "pubkey_ed25519" to userED25519KeyPair.publicKey.asHexString, - "timestamp" to timestamp, - "signature" to signAndEncode(verificationData, userED25519KeyPair), - "namespace" to Namespace.ALL, - ) - invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey).map { - rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse) - }.fail { e -> - Log.e("Loki", "Failed to clear data", e) + val deleteMessageParams = buildMap { + this["pubkey"] = userPublicKey + this["pubkey_ed25519"] = userED25519KeyPair.publicKey.asHexString + this["timestamp"] = timestamp + this["signature"] = signAndEncode(verificationData, userED25519KeyPair) + this["namespace"] = Namespace.ALL } + invoke(Snode.Method.DeleteAll, snode, deleteMessageParams, userPublicKey) + .map { rawResponse -> parseDeletions(userPublicKey, timestamp, rawResponse) } + .fail { e -> Log.e("Loki", "Failed to clear data", e) } } } } @@ -662,69 +651,53 @@ object SnodeAPI { fun parseRawMessagesResponse(rawResponse: RawResponse, snode: Snode, publicKey: String, namespace: Int = 0, updateLatestHash: Boolean = true, updateStoredHashes: Boolean = true): List> = (rawResponse["messages"] as? List<*>)?.let { messages -> - if (updateLatestHash) { - updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace) - } + if (updateLatestHash) updateLastMessageHashValueIfPossible(snode, publicKey, messages, namespace) removeDuplicates(publicKey, messages, namespace, updateStoredHashes).let(::parseEnvelopes) } ?: listOf() fun updateLastMessageHashValueIfPossible(snode: Snode, publicKey: String, rawMessages: List<*>, namespace: Int) { val lastMessageAsJSON = rawMessages.lastOrNull() as? Map<*, *> val hashValue = lastMessageAsJSON?.get("hash") as? String - if (hashValue != null) { - database.setLastMessageHashValue(snode, publicKey, hashValue, namespace) - } else if (rawMessages.isNotEmpty()) { - Log.d("Loki", "Failed to update last message hash value from: ${rawMessages.prettifiedDescription()}.") + when { + hashValue != null -> database.setLastMessageHashValue(snode, publicKey, hashValue, namespace) + rawMessages.isNotEmpty() -> Log.d("Loki", "Failed to update last message hash value from: ${rawMessages.prettifiedDescription()}.") } } fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int, updateStoredHashes: Boolean): List<*> { val originalMessageHashValues = database.getReceivedMessageHashValues(publicKey, namespace) ?: emptySet() val receivedMessageHashValues = originalMessageHashValues.toMutableSet() - val result = rawMessages.filter { rawMessage -> + return rawMessages.filter { rawMessage -> (rawMessage as? Map<*, *>) ?.let { it["hash"] as? String } ?.let { receivedMessageHashValues.add(it) } ?: false.also { Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.") } - } - if (updateStoredHashes && originalMessageHashValues.containsAll(receivedMessageHashValues)) { - database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues, namespace) - } - return result - } - - private fun parseEnvelopes(rawMessages: List<*>): List> = - rawMessages.mapNotNull { rawMessage -> - val rawMessageAsJSON = rawMessage as? Map<*, *> - val base64EncodedData = rawMessageAsJSON?.get("data") as? String - val data = base64EncodedData?.let { Base64.decode(it) } - - data?.runCatching(MessageWrapper::unwrap) - ?.map { it to rawMessageAsJSON["hash"] as? String } - ?.onFailure { Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") } - - if (data != null) { - try { - MessageWrapper.unwrap(data) to rawMessageAsJSON["hash"] as? String - } catch (e: Exception) { - Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") - null - } - } else { - Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.") - null + }.also { + if (updateStoredHashes && originalMessageHashValues.containsAll(receivedMessageHashValues)) { + database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues, namespace) } } + } + + private fun parseEnvelopes(rawMessages: List<*>): List> = rawMessages.mapNotNull { rawMessage -> + val rawMessageAsJSON = rawMessage as? Map<*, *> + val base64EncodedData = rawMessageAsJSON?.get("data") as? String + val data = base64EncodedData?.let { Base64.decode(it) } + + data ?: Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.") + + data?.runCatching { MessageWrapper.unwrap(this) to rawMessageAsJSON["hash"] as? String } + ?.onFailure { Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") } + ?.getOrNull() + } @Suppress("UNCHECKED_CAST") - private fun parseDeletions(userPublicKey: String, timestamp: Long, rawResponse: RawResponse): Map { - val swarms = rawResponse["swarm"] as? Map ?: return mapOf() - return swarms.mapNotNull { (hexSnodePublicKey, rawJSON) -> - val json = rawJSON as? Map ?: return@mapNotNull null - val isFailed = json["failed"] as? Boolean ?: false - val statusCode = json["code"] as? String - val reason = json["reason"] as? String - hexSnodePublicKey to if (isFailed) { + private fun parseDeletions(userPublicKey: String, timestamp: Long, rawResponse: RawResponse): Map = + (rawResponse["swarm"] as? Map)?.mapValuesNotNull { (hexSnodePublicKey, rawJSON) -> + val json = rawJSON as? Map ?: return@mapValuesNotNull null + if (json["failed"] as? Boolean == true) { + val reason = json["reason"] as? String + val statusCode = json["code"] as? String Log.e("Loki", "Failed to delete all messages from: $hexSnodePublicKey due to error: $reason ($statusCode).") false } else { @@ -735,8 +708,7 @@ object SnodeAPI { val message = sequenceOf(userPublicKey, "$timestamp").plus(hashes).toByteArray() sodium.cryptoSignVerifyDetached(Base64.decode(signature), message, message.size, snodePublicKey.asBytes) } - }.toMap() - } + } ?: mapOf() // endregion @@ -752,7 +724,7 @@ object SnodeAPI { publicKey?.let { dropSnodeFromSwarmIfNeeded(snode, it) } snodePool -= snode Log.d("Loki", "Snode pool count: ${snodePool.count()}.") - snodeFailureCount[snode] = 0 + snodeFailureCount.remove(snode) } } when (statusCode) { @@ -765,15 +737,14 @@ object SnodeAPI { } 421 -> { // The snode isn't associated with the given public key anymore - if (publicKey != null) { - json?.let(::parseSnodes) - ?.takeIf { it.isNotEmpty() } - ?.let { database.setSwarm(publicKey, it.toSet()) } - ?: run { - Log.d("Loki", "Invalidating swarm for: $publicKey.") - dropSnodeFromSwarmIfNeeded(snode, publicKey) - } - } else Log.d("Loki", "Got a 421 without an associated public key.") + if (publicKey == null) Log.d("Loki", "Got a 421 without an associated public key.") + else json?.let(::parseSnodes) + ?.takeIf { it.isNotEmpty() } + ?.let { database.setSwarm(publicKey, it.toSet()) } + ?: run { + Log.d("Loki", "Invalidating swarm for: $publicKey.") + dropSnodeFromSwarmIfNeeded(snode, publicKey) + } } 404 -> { Log.d("Loki", "404, probably no file found") 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 d47754b7ed..bc533d235f 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/Util.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/Util.kt @@ -402,6 +402,12 @@ fun Iterable.associateByNotNull( for(e in this) { it[keySelector(e) ?: continue] = valueTransform(e) ?: continue } } +fun Map.mapValuesNotNull( + valueTransform: (Map.Entry) -> W? +): Map = mutableMapOf().also { + for(e in this) { it[e.key] = valueTransform(e) ?: continue } +} + /** * Groups elements of the original collection by the key returned by the given [keySelector] function * applied to each element and returns a map where each group key is associated with a list of @@ -413,6 +419,21 @@ inline fun Iterable.groupByNotNull(keySelector: (E) -> K?): Map keySelector(e)?.let { k -> it.getOrPut(k) { mutableListOf() } += e } } } +/** + * Analogous to [buildMap], this function creates a [MutableMap] and populates it using the given [action]. + */ +inline fun buildMutableMap(action: MutableMap.() -> Unit): MutableMap = + mutableMapOf().apply(action) + +/** + * Converts a list of Pairs into a Map, filtering out any Pairs where the value is null. + * + * @param pairs The list of Pairs to convert. + * @return A Map with non-null values. + */ +fun Iterable>.toMapNotNull(): Map = + associateByNotNull(Pair::first, Pair::second) + fun Sequence.toByteArray(): ByteArray = ByteArrayOutputStream().use { output -> forEach { it.byteInputStream().use { input -> input.copyTo(output) } } output.toByteArray() From 3c8302f7a4ce226096b2cac79b0c81d11fee949a Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Sat, 3 Aug 2024 21:19:31 +0930 Subject: [PATCH 36/45] Optimise SnodeAPI further --- .../java/org/session/libsession/snode/SnodeAPI.kt | 11 +++++------ .../java/org/session/libsignal/utilities/Snode.kt | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) 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 6d1ffaa5c0..4d59669d1c 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -455,7 +455,7 @@ object SnodeAPI { val hashes = messageHashes.takeIf { it.size != 1 } ?: (messageHashes + "///////////////////////////////////////////") // TODO remove this when bug is fixed on nodes. return retryIfNeeded(maxRetryCount) { val timestamp = System.currentTimeMillis() + clockOffset - val signData = "${Snode.Method.GetExpiries.rawValue}$timestamp${hashes.joinToString(separator = "")}".toByteArray() + val signData = sequenceOf(Snode.Method.GetExpiries.rawValue).plus(timestamp.toString()).plus(hashes).toByteArray() val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString val signature = try { @@ -499,7 +499,7 @@ object SnodeAPI { val shortenOrExtend = if (extend) "extend" else if (shorten) "shorten" else "" - val signData = "${Snode.Method.Expire.rawValue}$shortenOrExtend$newExpiry${messageHashes.joinToString(separator = "")}".toByteArray() + val signData = sequenceOf(Snode.Method.Expire.rawValue).plus(shortenOrExtend).plus(newExpiry.toString()).plus(messageHashes).toByteArray() val signature = try { signAndEncode(signData, userEd25519KeyPair) @@ -633,7 +633,7 @@ object SnodeAPI { getSingleTargetSnode(userPublicKey).bind { snode -> retryIfNeeded(maxRetryCount) { getNetworkTime(snode).bind { (_, timestamp) -> - val verificationData = (Snode.Method.DeleteAll.rawValue + Namespace.ALL + timestamp.toString()).toByteArray() + val verificationData = sequenceOf(Snode.Method.DeleteAll.rawValue, Namespace.ALL, timestamp.toString()).toByteArray() val deleteMessageParams = buildMap { this["pubkey"] = userPublicKey this["pubkey_ed25519"] = userED25519KeyPair.publicKey.asHexString @@ -722,9 +722,8 @@ object SnodeAPI { if (newFailureCount >= snodeFailureThreshold) { Log.d("Loki", "Failure threshold reached for: $snode; dropping it.") publicKey?.let { dropSnodeFromSwarmIfNeeded(snode, it) } - snodePool -= snode - Log.d("Loki", "Snode pool count: ${snodePool.count()}.") - snodeFailureCount.remove(snode) + snodePool = (snodePool - snode).also { Log.d("Loki", "Snode pool count: ${it.count()}.") } + snodeFailureCount -= snode } } when (statusCode) { 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 cc123a8527..f918dbbf73 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt @@ -52,6 +52,8 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val v fun Version(value: String) = CACHE.getOrElse(value) { Snode.Version(value) } + + fun Version(parts: List) = Version(parts.joinToString(".")) } @JvmInline @@ -66,14 +68,12 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val v } } - constructor(parts: List): this( + internal constructor(parts: List): this( parts.asSequence() .map { it.toByte().toULong() } .foldToVersionAsULong() ) - constructor(value: Int): this(value.toULong()) - internal constructor(value: String): this( value.splitToSequence(".") .map { it.toULongOrNull() ?: 0UL } From 6e1ed8cc117eb5706c8c4d97a33f1926af33777a Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Sat, 3 Aug 2024 21:41:45 +0930 Subject: [PATCH 37/45] Add SnodeTest --- .../session/libsignal/utilities/SnodeTest.kt | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 app/src/test/java/org/session/libsignal/utilities/SnodeTest.kt diff --git a/app/src/test/java/org/session/libsignal/utilities/SnodeTest.kt b/app/src/test/java/org/session/libsignal/utilities/SnodeTest.kt new file mode 100644 index 0000000000..d778db6519 --- /dev/null +++ b/app/src/test/java/org/session/libsignal/utilities/SnodeTest.kt @@ -0,0 +1,53 @@ +package org.session.libsignal.utilities + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.core.IsEqual.equalTo +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class SnodeVersionTest( + private val v1: String, + private val v2: String, + private val expectedEqual: Boolean, + private val expectedLessThan: Boolean +) { + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{index}: testVersion({0},{1}) = (equalTo: {2}, lessThan: {3})") + fun data(): Collection> = listOf( + arrayOf("1", "1", true, false), + arrayOf("1", "2", false, true), + arrayOf("2", "1", false, false), + arrayOf("1.0", "1", true, false), + arrayOf("1.0", "1.0.0", true, false), + arrayOf("1.0", "1.0.0.0", true, false), + arrayOf("1.0", "1.0.0.0.0.0", true, false), + arrayOf("2.0", "1.2", false, false), + arrayOf("1.0.0.0", "1.0.0.1", false, true), + // Snode.Version only considers the first 4 integers, so these are equal + arrayOf("1.0.0.0", "1.0.0.0.1", true, false), + arrayOf("1.0.0.1", "1.0.0.1", true, false), + arrayOf("12345.12345.12345.12345", "12345.12345.12345.12345", true, false), + arrayOf("11111.11111.11111.11111", "11111.11111.11111.99999", false, true), + arrayOf("11111.11111.11111.11111", "11111.11111.99999.99999", false, true), + arrayOf("11111.11111.11111.11111", "11111.99999.99999.99999", false, true), + arrayOf("11111.11111.11111.11111", "99999.99999.99999.99999", false, true), + ) + } + + @Test + fun testVersionEqual() { + val version1 = Snode.Version(v1) + val version2 = Snode.Version(v2) + assertThat(version1 == version2, equalTo(expectedEqual)) + } + + @Test + fun testVersionOnePartLessThan() { + val version1 = Snode.Version(v1) + val version2 = Snode.Version(v2) + assertThat(version1 < version2, equalTo(expectedLessThan)) + } +} \ No newline at end of file From 541766099680dbf80c60767813bf8fc2accf9dde Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Sat, 3 Aug 2024 21:52:01 +0930 Subject: [PATCH 38/45] Improve Version to cap parts at 16-bits rather than masking them --- .../java/org/session/libsignal/utilities/SnodeTest.kt | 9 ++++----- .../main/java/org/session/libsignal/utilities/Snode.kt | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/test/java/org/session/libsignal/utilities/SnodeTest.kt b/app/src/test/java/org/session/libsignal/utilities/SnodeTest.kt index d778db6519..aed54fd0d3 100644 --- a/app/src/test/java/org/session/libsignal/utilities/SnodeTest.kt +++ b/app/src/test/java/org/session/libsignal/utilities/SnodeTest.kt @@ -29,11 +29,10 @@ class SnodeVersionTest( // Snode.Version only considers the first 4 integers, so these are equal arrayOf("1.0.0.0", "1.0.0.0.1", true, false), arrayOf("1.0.0.1", "1.0.0.1", true, false), - arrayOf("12345.12345.12345.12345", "12345.12345.12345.12345", true, false), - arrayOf("11111.11111.11111.11111", "11111.11111.11111.99999", false, true), - arrayOf("11111.11111.11111.11111", "11111.11111.99999.99999", false, true), - arrayOf("11111.11111.11111.11111", "11111.99999.99999.99999", false, true), - arrayOf("11111.11111.11111.11111", "99999.99999.99999.99999", false, true), + // parts can be up to 16 bits, around 65,535 + arrayOf("65535.65535.65535.65535", "65535.65535.65535.65535", true, false), + // values higher than this are coerced to 65535 (: + arrayOf("65535.65535.65535.65535", "65535.65535.65535.99999", true, false), ) } 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 f918dbbf73..86f3e26b64 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt @@ -64,7 +64,7 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val v private const val MASK = 0xFFFFUL private fun Sequence.foldToVersionAsULong() = take(4).foldIndexed(0UL) { i, acc, it -> - it and MASK shl (3 - i) * MASK_BITS or acc + it.coerceAtMost(MASK) shl (3 - i) * MASK_BITS or acc } } From 2125502e771e8ff35948e6f0e63a5cf402a3712c Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Sat, 3 Aug 2024 22:08:17 +0930 Subject: [PATCH 39/45] Refactor a few MessagingModuleConfiguration function calls --- .../org/session/libsession/snode/SnodeAPI.kt | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) 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 4d59669d1c..4a2a9f2134 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -287,6 +287,10 @@ object SnodeAPI { sodium.cryptoSignDetached(it, data, data.size.toLong(), userED25519KeyPair.secretKey.asBytes) } + private fun getUserED25519KeyPairCatchingOrNull() = runCatching { MessagingModuleConfiguration.shared.getUserED25519KeyPair() }.getOrNull() + private fun getUserED25519KeyPair(): KeyPair? = MessagingModuleConfiguration.shared.getUserED25519KeyPair() + private fun getUserPublicKey() = MessagingModuleConfiguration.shared.storage.getUserPublicKey() + fun getRawMessages(snode: Snode, publicKey: String, requiresAuth: Boolean = true, namespace: Int = 0): RawResponsePromise { // Get last message hash val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: "" @@ -300,8 +304,7 @@ object SnodeAPI { // Construct signature if (requiresAuth) { val userED25519KeyPair = try { - MessagingModuleConfiguration.shared.getUserED25519KeyPair() - ?: return Promise.ofFail(Error.NoKeyPair) + getUserED25519KeyPair() ?: return Promise.ofFail(Error.NoKeyPair) } catch (e: Exception) { Log.e("Loki", "Error getting KeyPair", e) return Promise.ofFail(Error.NoKeyPair) @@ -327,7 +330,7 @@ object SnodeAPI { // used for sig generation since it is also the value used in timestamp parameter val messageTimestamp = message.timestamp - val userED25519KeyPair = runCatching { MessagingModuleConfiguration.shared.getUserED25519KeyPair() }.getOrNull() ?: return null + val userED25519KeyPair = getUserED25519KeyPairCatchingOrNull() ?: return null val verificationData = "store$namespace$messageTimestamp".toByteArray() val signature = signAndEncodeCatching(verificationData, userED25519KeyPair).run { @@ -362,11 +365,7 @@ object SnodeAPI { * @param required indicates that *at least one* message in the list is deleted from the server, otherwise it will return 404 */ fun buildAuthenticatedDeleteBatchInfo(publicKey: String, messageHashes: List, required: Boolean = false): SnodeBatchRequestInfo? { - val userEd25519KeyPair = try { - MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null - } catch (e: Exception) { - return null - } + val userEd25519KeyPair = getUserED25519KeyPairCatchingOrNull() ?: return null val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString val verificationData = sequenceOf("delete").plus(messageHashes).toByteArray() val signature = try { @@ -391,7 +390,7 @@ object SnodeAPI { fun buildAuthenticatedRetrieveBatchRequest(snode: Snode, publicKey: String, namespace: Int = 0, maxSize: Int? = null): SnodeBatchRequestInfo? { val lastHashValue = database.getLastMessageHashValue(snode, publicKey, namespace) ?: "" - val userEd25519KeyPair = runCatching { MessagingModuleConfiguration.shared.getUserED25519KeyPair() }.getOrNull() ?: return null + val userEd25519KeyPair = getUserED25519KeyPairCatchingOrNull() ?: return null val ed25519PublicKey = userEd25519KeyPair.publicKey.asHexString val timestamp = System.currentTimeMillis() + clockOffset val verificationData = if (namespace == 0) "retrieve$timestamp".toByteArray() @@ -451,7 +450,7 @@ object SnodeAPI { } fun getExpiries(messageHashes: List, publicKey: String) : RawResponsePromise { - val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(NullPointerException("No user key pair")) + val userEd25519KeyPair = getUserED25519KeyPairCatchingOrNull() ?: return Promise.ofFail(NullPointerException("No user key pair")) val hashes = messageHashes.takeIf { it.size != 1 } ?: (messageHashes + "///////////////////////////////////////////") // TODO remove this when bug is fixed on nodes. return retryIfNeeded(maxRetryCount) { val timestamp = System.currentTimeMillis() + clockOffset @@ -495,7 +494,7 @@ object SnodeAPI { extend: Boolean = false, shorten: Boolean = false ): Map? { - val userEd25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null + val userEd25519KeyPair = getUserED25519KeyPairCatchingOrNull() ?: return null val shortenOrExtend = if (extend) "extend" else if (shorten) "shorten" else "" @@ -535,8 +534,7 @@ object SnodeAPI { fun sendMessage(message: SnodeMessage, requiresAuth: Boolean = false, namespace: Int = 0): RawResponsePromise = retryIfNeeded(maxRetryCount) { - val module = MessagingModuleConfiguration.shared - val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) + val userED25519KeyPair = getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) val parameters = message.toJSON().toMutableMap() // Construct signature if (requiresAuth) { @@ -566,9 +564,8 @@ object SnodeAPI { fun deleteMessage(publicKey: String, serverHashes: List): Promise, Exception> = retryIfNeeded(maxRetryCount) { - val module = MessagingModuleConfiguration.shared - val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) - val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) + val userED25519KeyPair = getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) + val userPublicKey = getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) getSingleTargetSnode(publicKey).bind { snode -> retryIfNeeded(maxRetryCount) { val verificationData = sequenceOf(Snode.Method.DeleteMessage.rawValue).plus(serverHashes).toByteArray() @@ -627,9 +624,8 @@ object SnodeAPI { fun deleteAllMessages(): Promise, Exception> = retryIfNeeded(maxRetryCount) { - val module = MessagingModuleConfiguration.shared - val userED25519KeyPair = module.getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) - val userPublicKey = module.storage.getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) + val userED25519KeyPair = getUserED25519KeyPair() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) + val userPublicKey = getUserPublicKey() ?: return@retryIfNeeded Promise.ofFail(Error.NoKeyPair) getSingleTargetSnode(userPublicKey).bind { snode -> retryIfNeeded(maxRetryCount) { getNetworkTime(snode).bind { (_, timestamp) -> From 61cb602e63be3ee7093c035c91adfa0ea03f184b Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Sat, 3 Aug 2024 22:49:23 +0930 Subject: [PATCH 40/45] Simplify and document some functions --- .../org/session/libsession/snode/SnodeAPI.kt | 5 +-- .../org/session/libsignal/utilities/Snode.kt | 35 ++++++------------- 2 files changed, 12 insertions(+), 28 deletions(-) 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 4a2a9f2134..9c584ba0fb 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -736,10 +736,7 @@ object SnodeAPI { else json?.let(::parseSnodes) ?.takeIf { it.isNotEmpty() } ?.let { database.setSwarm(publicKey, it.toSet()) } - ?: run { - Log.d("Loki", "Invalidating swarm for: $publicKey.") - dropSnodeFromSwarmIfNeeded(snode, publicKey) - } + ?: dropSnodeFromSwarmIfNeeded(snode, publicKey).also { Log.d("Loki", "Invalidating swarm for: $publicKey.") } } 404 -> { Log.d("Loki", "404, probably no file found") 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 86f3e26b64..58dcbe8a37 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt @@ -2,6 +2,9 @@ package org.session.libsignal.utilities import android.annotation.SuppressLint +/** + * Create a Snode from a "-" delimited String if valid, null otherwise. + */ fun Snode(string: String): Snode? { val components = string.split("-") val address = components[0] @@ -31,25 +34,16 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val v data class KeySet(val ed25519Key: String, val x25519Key: String) - override fun equals(other: Any?): Boolean { - return if (other is Snode) { - address == other.address && port == other.port - } else { - false - } - } - - override fun hashCode(): Int { - return address.hashCode() xor port.hashCode() - } - - override fun toString(): String { return "$address:$port" } + override fun equals(other: Any?) = other is Snode && address == other.address && port == other.port + override fun hashCode(): Int = address.hashCode() xor port.hashCode() + override fun toString(): String = "$address:$port" companion object { private val CACHE = mutableMapOf() @SuppressLint("NotConstructor") fun Version(value: String) = CACHE.getOrElse(value) { + // internal constructor takes precedence Snode.Version(value) } @@ -62,22 +56,15 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val v val ZERO = Version(0UL) private const val MASK_BITS = 16 private const val MASK = 0xFFFFUL - - private fun Sequence.foldToVersionAsULong() = take(4).foldIndexed(0UL) { i, acc, it -> - it.coerceAtMost(MASK) shl (3 - i) * MASK_BITS or acc - } } - internal constructor(parts: List): this( - parts.asSequence() - .map { it.toByte().toULong() } - .foldToVersionAsULong() - ) - internal constructor(value: String): this( value.splitToSequence(".") + .take(4) .map { it.toULongOrNull() ?: 0UL } - .foldToVersionAsULong() + .foldIndexed(0UL) { i, acc, it -> + it.coerceAtMost(MASK) shl (3 - i) * MASK_BITS or acc + } ) operator fun compareTo(other: Version): Int = value.compareTo(other.value) From f5d1debc323a889067f44a7945159a54034ee4d9 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Sun, 4 Aug 2024 00:18:47 +0930 Subject: [PATCH 41/45] Add shared SecureRandom instance --- .../securesms/conversation/v2/Util.kt | 34 ++----------------- .../crypto/AttachmentSecretProvider.java | 7 ++-- .../crypto/DatabaseSecretProvider.java | 6 ++-- .../ModernEncryptingPartOutputStream.java | 5 +-- .../securesms/database/GroupDatabase.java | 11 ++---- .../securesms/database/MmsDatabase.kt | 5 ++- .../securesms/database/SmsDatabase.java | 5 +-- .../glide/PaddedHeadersInterceptor.java | 11 +++--- .../securesms/logging/LogFile.java | 4 +-- .../securesms/logging/LogSecretProvider.java | 6 ++-- .../org/thoughtcrime/securesms/mms/Slide.kt | 4 +-- .../securesms/net/ChunkedDataFetcher.java | 5 +-- .../securesms/permissions/Permissions.java | 11 ++---- .../securesms/preferences/SettingsActivity.kt | 4 +-- .../securesms/webrtc/PeerConnectionWrapper.kt | 7 ++-- .../jobs/RetrieveProfileAvatarJob.kt | 6 ++-- .../pollers/ClosedGroupPollerV2.kt | 4 +-- .../sending_receiving/pollers/Poller.kt | 6 ++-- .../messaging/utilities/MessageWrapper.kt | 20 +++++------ .../libsession/snode/OnionRequestAPI.kt | 15 ++++---- .../org/session/libsession/snode/SnodeAPI.kt | 12 ++++--- .../org/session/libsession/utilities/Util.kt | 20 ++++------- .../org/session/libsignal/crypto/Random.kt | 14 +++++--- .../streams/ProfileCipherOutputStream.java | 4 +-- .../org/session/libsignal/utilities/HTTP.kt | 7 ++-- .../org/session/libsignal/utilities/Util.java | 20 ++--------- 26 files changed, 98 insertions(+), 155 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.kt index ff33a58e91..71a0aae0fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.kt @@ -33,18 +33,16 @@ import android.view.View import com.annimon.stream.Stream import com.google.android.mms.pdu_alt.CharacterSets import com.google.android.mms.pdu_alt.EncodedStringValue +import network.loki.messenger.R +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.components.ComposeText import java.io.ByteArrayOutputStream import java.io.IOException import java.io.UnsupportedEncodingException -import java.security.SecureRandom -import java.util.Arrays import java.util.Collections import java.util.concurrent.TimeUnit import kotlin.math.max import kotlin.math.min -import network.loki.messenger.R -import org.session.libsignal.utilities.Log -import org.thoughtcrime.securesms.components.ComposeText object Util { private val TAG: String = Log.tag(Util::class.java) @@ -248,32 +246,6 @@ object Util { return result } - fun getSecretBytes(size: Int): ByteArray { - return getSecretBytes(SecureRandom(), size) - } - - fun getSecretBytes(secureRandom: SecureRandom, size: Int): ByteArray { - val secret = ByteArray(size) - secureRandom.nextBytes(secret) - return secret - } - - fun getRandomElement(elements: Array): T { - return elements[SecureRandom().nextInt(elements.size)] - } - - fun getRandomElement(elements: List): T { - return elements[SecureRandom().nextInt(elements.size)] - } - - fun equals(a: Any?, b: Any?): Boolean { - return a === b || (a != null && a == b) - } - - fun hashCode(vararg objects: Any?): Int { - return objects.contentHashCode() - } - fun uri(uri: String?): Uri? { return if (uri == null) null else Uri.parse(uri) diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.java index 0344551cab..1602ab3fcf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/AttachmentSecretProvider.java @@ -1,14 +1,14 @@ package org.thoughtcrime.securesms.crypto; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; + import android.content.Context; import android.os.Build; import androidx.annotation.NonNull; import org.session.libsession.utilities.TextSecurePreferences; -import java.security.SecureRandom; - /** * A provider that is responsible for creating or retrieving the AttachmentSecret model. * @@ -81,9 +81,8 @@ public class AttachmentSecretProvider { } private AttachmentSecret createAndStoreAttachmentSecret(@NonNull Context context) { - SecureRandom random = new SecureRandom(); byte[] secret = new byte[32]; - random.nextBytes(secret); + SECURE_RANDOM.nextBytes(secret); AttachmentSecret attachmentSecret = new AttachmentSecret(null, null, secret); storeAttachmentSecret(context, attachmentSecret); diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java index 0ad22c3a90..44bb33161c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/DatabaseSecretProvider.java @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.crypto; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; + import android.content.Context; import android.os.Build; import androidx.annotation.NonNull; @@ -8,7 +10,6 @@ import androidx.annotation.NonNull; import org.session.libsession.utilities.TextSecurePreferences; import java.io.IOException; -import java.security.SecureRandom; public class DatabaseSecretProvider { @@ -60,9 +61,8 @@ public class DatabaseSecretProvider { } private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) { - SecureRandom random = new SecureRandom(); byte[] secret = new byte[32]; - random.nextBytes(secret); + SECURE_RANDOM.nextBytes(secret); DatabaseSecret databaseSecret = new DatabaseSecret(secret); diff --git a/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java b/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java index ab4efe84a9..0bd62f2143 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java +++ b/app/src/main/java/org/thoughtcrime/securesms/crypto/ModernEncryptingPartOutputStream.java @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.crypto; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; + import androidx.annotation.NonNull; import android.util.Pair; @@ -11,7 +13,6 @@ import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import javax.crypto.Cipher; import javax.crypto.CipherOutputStream; @@ -31,7 +32,7 @@ public class ModernEncryptingPartOutputStream { throws IOException { byte[] random = new byte[32]; - new SecureRandom().nextBytes(random); + SECURE_RANDOM.nextBytes(random); try { Mac mac = Mac.getInstance("HmacSHA256"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 66d01114ef..6e1f72c568 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.database; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; + import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; @@ -26,7 +28,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.util.BitmapUtil; import java.io.Closeable; -import java.security.SecureRandom; import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -303,7 +304,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt public void updateProfilePicture(String groupID, byte[] newValue) { long avatarId; - if (newValue != null) avatarId = Math.abs(new SecureRandom().nextLong()); + if (newValue != null) avatarId = Math.abs(SECURE_RANDOM.nextLong()); else avatarId = 0; @@ -458,12 +459,6 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId}); } - public byte[] allocateGroupId() { - byte[] groupId = new byte[16]; - new SecureRandom().nextBytes(groupId); - return groupId; - } - public boolean hasGroup(@NonNull String groupId) { try (Cursor cursor = databaseHelper.getReadableDatabase().rawQuery( "SELECT 1 FROM " + TABLE_NAME + " WHERE " + GROUP_ID + " = ? LIMIT 1", diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index 9d3a6c9c18..5a2a9155de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -46,11 +46,11 @@ import org.session.libsession.utilities.IdentityKeyMismatchList import org.session.libsession.utilities.NetworkFailure import org.session.libsession.utilities.NetworkFailureList import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled -import org.session.libsession.utilities.Util.toIsoBytes import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ThreadUtils.queue +import org.session.libsignal.utilities.Util.SECURE_RANDOM import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener @@ -66,7 +66,6 @@ import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.util.asSequence import java.io.Closeable import java.io.IOException -import java.security.SecureRandom import java.util.LinkedList class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : MessagingDatabase(context, databaseHelper) { @@ -1200,7 +1199,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa inner class OutgoingMessageReader(private val message: OutgoingMediaMessage?, private val threadId: Long) { - private val id = SecureRandom().nextLong() + private val id = SECURE_RANDOM.nextLong() val current: MessageRecord get() { val slideDeck = SlideDeck(context, message!!.attachments) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 84b9441834..f02498112f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -17,6 +17,8 @@ */ package org.thoughtcrime.securesms.database; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; + import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -49,7 +51,6 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import java.io.Closeable; import java.io.IOException; -import java.security.SecureRandom; import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; @@ -784,7 +785,7 @@ public class SmsDatabase extends MessagingDatabase { public OutgoingMessageReader(OutgoingTextMessage message, long threadId) { this.message = message; this.threadId = threadId; - this.id = new SecureRandom().nextLong(); + this.id = SECURE_RANDOM.nextLong(); } public MessageRecord getCurrent() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/PaddedHeadersInterceptor.java b/app/src/main/java/org/thoughtcrime/securesms/glide/PaddedHeadersInterceptor.java index 5d0ab584b7..9c347eb414 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/PaddedHeadersInterceptor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/PaddedHeadersInterceptor.java @@ -1,9 +1,10 @@ package org.thoughtcrime.securesms.glide; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; + import androidx.annotation.NonNull; import java.io.IOException; -import java.security.SecureRandom; import okhttp3.Headers; import okhttp3.Interceptor; @@ -30,15 +31,15 @@ public class PaddedHeadersInterceptor implements Interceptor { private @NonNull Headers getPaddedHeaders(@NonNull Headers headers) { return headers.newBuilder() - .add(PADDING_HEADER, getRandomString(new SecureRandom(), MIN_RANDOM_BYTES, MAX_RANDOM_BYTES)) + .add(PADDING_HEADER, getRandomString(MIN_RANDOM_BYTES, MAX_RANDOM_BYTES)) .build(); } - private static @NonNull String getRandomString(@NonNull SecureRandom secureRandom, int minLength, int maxLength) { - char[] buffer = new char[secureRandom.nextInt(maxLength - minLength) + minLength]; + private static @NonNull String getRandomString(int minLength, int maxLength) { + char[] buffer = new char[SECURE_RANDOM.nextInt(maxLength - minLength) + minLength]; for (int i = 0 ; i < buffer.length; i++) { - buffer[i] = (char) (secureRandom.nextInt(74) + 48); // Random char from 0-Z + buffer[i] = (char) (SECURE_RANDOM.nextInt(74) + 48); // Random char from 0-Z } return new String(buffer); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java b/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java index 909f19e08c..cfe6cc3809 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.logging; import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; import androidx.annotation.NonNull; @@ -17,7 +18,6 @@ import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -64,7 +64,7 @@ class LogFile { } void writeEntry(@NonNull String entry) throws IOException { - new SecureRandom().nextBytes(ivBuffer); + SECURE_RANDOM.nextBytes(ivBuffer); byte[] plaintext = entry.getBytes(); try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.java b/app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.java index aabd2811db..763a2a430d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/LogSecretProvider.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.logging; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; + import android.content.Context; import android.os.Build; import androidx.annotation.NonNull; @@ -9,7 +11,6 @@ import org.session.libsignal.utilities.Base64; import org.session.libsession.utilities.TextSecurePreferences; import java.io.IOException; -import java.security.SecureRandom; class LogSecretProvider { @@ -40,9 +41,8 @@ class LogSecretProvider { } private static byte[] createAndStoreSecret(@NonNull Context context) { - SecureRandom random = new SecureRandom(); byte[] secret = new byte[32]; - random.nextBytes(secret); + SECURE_RANDOM.nextBytes(secret); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(secret); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt index e284c1ce22..05d8167d9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/Slide.kt @@ -21,7 +21,6 @@ import android.content.res.Resources import android.net.Uri import androidx.annotation.DrawableRes import com.squareup.phrase.Phrase -import java.security.SecureRandom import network.loki.messenger.R import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress @@ -29,6 +28,7 @@ import org.session.libsession.messaging.sending_receiving.attachments.UriAttachm import org.session.libsession.utilities.StringSubstitutionConstants.EMOJI_KEY import org.session.libsession.utilities.Util.equals import org.session.libsession.utilities.Util.hashCode +import org.session.libsignal.utilities.Util.SECURE_RANDOM import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.conversation.v2.Util import org.thoughtcrime.securesms.util.MediaUtil @@ -160,7 +160,7 @@ abstract class Slide(@JvmField protected val context: Context, protected val att ): Attachment { val resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime) - val fastPreflightId = SecureRandom().nextLong().toString() + val fastPreflightId = SECURE_RANDOM.nextLong().toString() return UriAttachment( uri, if (hasThumbnail) uri else null, diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/ChunkedDataFetcher.java b/app/src/main/java/org/thoughtcrime/securesms/net/ChunkedDataFetcher.java index af37b86394..e50846b1c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/net/ChunkedDataFetcher.java +++ b/app/src/main/java/org/thoughtcrime/securesms/net/ChunkedDataFetcher.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.net; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; + import androidx.annotation.NonNull; import android.text.TextUtils; @@ -15,7 +17,6 @@ import org.session.libsignal.utilities.guava.Optional; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; -import java.security.SecureRandom; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -54,7 +55,7 @@ public class ChunkedDataFetcher { private RequestController fetchChunksWithUnknownTotalSize(@NonNull String url, @NonNull Callback callback) { CompositeRequestController compositeController = new CompositeRequestController(); - long chunkSize = new SecureRandom().nextInt(1024) + 1024; + long chunkSize = SECURE_RANDOM.nextInt(1024) + 1024; Request request = new Request.Builder() .url(url) .cacheControl(NO_CACHE) diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java index 88ee67cb4d..9b6950bf7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java @@ -1,9 +1,9 @@ package org.thoughtcrime.securesms.permissions; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; + import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; @@ -11,9 +11,7 @@ import android.os.Build; import android.provider.Settings; import android.util.DisplayMetrics; import android.view.Display; -import android.view.ViewGroup; import android.view.WindowManager; -import android.widget.Button; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; @@ -28,13 +26,10 @@ import org.session.libsession.utilities.ServiceUtil; import org.thoughtcrime.securesms.util.LRUCache; import java.lang.ref.WeakReference; -import java.security.SecureRandom; import java.util.Arrays; import java.util.List; import java.util.Map; -import network.loki.messenger.R; - public class Permissions { private static final Map OUTSTANDING = new LRUCache<>(2); @@ -172,7 +167,7 @@ public class Permissions { } private void executePermissionsRequest(PermissionsRequest request) { - int requestCode = new SecureRandom().nextInt(65434) + 100; + int requestCode = SECURE_RANDOM.nextInt(65434) + 100; synchronized (OUTSTANDING) { OUTSTANDING.put(requestCode, request); diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index fc98070ca4..dc1011d970 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -62,6 +62,7 @@ import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.truncateIdForDisplay import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.Util.SECURE_RANDOM import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.components.ProfilePictureView @@ -90,7 +91,6 @@ import org.thoughtcrime.securesms.util.NetworkUtils import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.show import java.io.File -import java.security.SecureRandom import javax.inject.Inject @AndroidEntryPoint @@ -294,7 +294,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { val userConfig = configFactory.user AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) - prefs.setProfileAvatarId(SecureRandom().nextInt() ) + prefs.setProfileAvatarId(SECURE_RANDOM.nextInt() ) ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) // Attempt to grab the details we require to update the profile picture diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt index b61edbb6d2..24ea2a390f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/PeerConnectionWrapper.kt @@ -1,8 +1,10 @@ package org.thoughtcrime.securesms.webrtc import android.content.Context +import org.session.libsignal.crypto.shuffledRandom import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.SettableFuture +import org.session.libsignal.utilities.Util.SECURE_RANDOM import org.thoughtcrime.securesms.webrtc.video.Camera import org.thoughtcrime.securesms.webrtc.video.CameraEventListener import org.thoughtcrime.securesms.webrtc.video.CameraState @@ -22,9 +24,7 @@ import org.webrtc.SurfaceTextureHelper import org.webrtc.VideoSink import org.webrtc.VideoSource import org.webrtc.VideoTrack -import java.security.SecureRandom import java.util.concurrent.ExecutionException -import kotlin.random.asKotlinRandom class PeerConnectionWrapper(private val context: Context, private val factory: PeerConnectionFactory, @@ -49,8 +49,7 @@ class PeerConnectionWrapper(private val context: Context, private var isInitiator = false private fun initPeerConnection() { - val random = SecureRandom().asKotlinRandom() - val iceServers = listOf("freyr","angus","hereford","holstein", "brahman").shuffled(random).take(2).map { sub -> + val iceServers = listOf("freyr","angus","hereford","holstein", "brahman").shuffledRandom().take(2).map { sub -> PeerConnection.IceServer.builder("turn:$sub.getsession.org") .setUsername("session202111") .setPassword("053c268164bc7bd7") diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt index 54efa1c80b..d82f29446d 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/RetrieveProfileAvatarJob.kt @@ -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) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPollerV2.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPollerV2.kt index 293fbc8d8a..c9b7abefbc 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPollerV2.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/ClosedGroupPollerV2.kt @@ -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 { 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 { diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 2b7c8159ea..f57aa1f0f7 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -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.") diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt index 569b9c62c1..71cd587b45 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/MessageWrapper.kt @@ -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 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 ead454eb44..9ff541a9d5 100644 --- a/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/OnionRequestAPI.kt @@ -10,11 +10,10 @@ 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 -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 @@ -149,7 +148,7 @@ object OnionRequestAPI { val reusableGuardSnodeCount = reusableGuardSnodes.count() if (unusedSnodes.count() < (targetGuardSnodeCount - reusableGuardSnodeCount)) { throw InsufficientSnodesException() } fun getGuardSnode(): Promise { - val candidate = unusedSnodes.getRandomElementOrNull() + val candidate = unusedSnodes.secureRandomOrNull() ?: return Promise.ofFail(InsufficientSnodesException()) unusedSnodes = unusedSnodes.minus(candidate) Log.d("Loki", "Testing guard snode: $candidate.") @@ -191,7 +190,7 @@ object OnionRequestAPI { // 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)).mapIndexed() { index, _ -> - var pathSnode = unusedSnodes.getRandomElement() + var pathSnode = unusedSnodes.secureRandom() // remove the snode from the unused list and return it unusedSnodes = unusedSnodes.minus(pathSnode) @@ -228,9 +227,9 @@ object OnionRequestAPI { OnionRequestAPI.guardSnodes = guardSnodes fun getPath(paths: List): Path { return if (snodeToExclude != null) { - paths.filter { !it.contains(snodeToExclude) }.getRandomElement() + paths.filter { !it.contains(snodeToExclude) }.secureRandom() } else { - paths.getRandomElement() + paths.secureRandom() } } when { @@ -273,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 ) 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 034ef0e7d2..6a27312f7c 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -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 @@ -209,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) @@ -224,7 +226,7 @@ object SnodeAPI { } return deferred.promise } else { - return Promise.of(snodePool.getRandomElement()) + return Promise.of(snodePool.secureRandom()) } } @@ -241,8 +243,8 @@ object SnodeAPI { } internal fun getSingleTargetSnode(publicKey: String): Promise { - // 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 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 929f53e305..1293547703 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/Util.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/Util.kt @@ -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 getRandomElement(elements: Array): T { - return elements[SecureRandom().nextInt(elements.size)] - } + fun getRandomElement(elements: Array): 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 diff --git a/libsignal/src/main/java/org/session/libsignal/crypto/Random.kt b/libsignal/src/main/java/org/session/libsignal/crypto/Random.kt index 4f7687307b..c32eb78595 100644 --- a/libsignal/src/main/java/org/session/libsignal/crypto/Random.kt +++ b/libsignal/src/main/java/org/session/libsignal/crypto/Random.kt @@ -1,19 +1,23 @@ package org.session.libsignal.crypto -import java.security.SecureRandom +import org.session.libsignal.utilities.Util.SECURE_RANDOM /** * Uses `SecureRandom` to pick an element from this collection. */ -fun Collection.getRandomElementOrNull(): T? { +fun Collection.secureRandomOrNull(): T? { if (isEmpty()) return null - val index = SecureRandom().nextInt(size) // SecureRandom() should be cryptographically secure + val index = SECURE_RANDOM.nextInt(size) // SecureRandom should be cryptographically secure return elementAtOrNull(index) } /** * Uses `SecureRandom` to pick an element from this collection. + * + * @throws [NullPointerException] if the [Collection] is empty */ -fun Collection.getRandomElement(): T { - return getRandomElementOrNull()!! +fun Collection.secureRandom(): T { + return secureRandomOrNull()!! } + +fun Collection.shuffledRandom(): List = shuffled(SECURE_RANDOM) diff --git a/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherOutputStream.java b/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherOutputStream.java index f47a5f72b6..43734605ae 100644 --- a/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherOutputStream.java +++ b/libsignal/src/main/java/org/session/libsignal/streams/ProfileCipherOutputStream.java @@ -1,13 +1,13 @@ package org.session.libsignal.streams; import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; +import static org.session.libsignal.utilities.Util.SECURE_RANDOM; import java.io.IOException; import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -80,7 +80,7 @@ public class ProfileCipherOutputStream extends DigestingOutputStream { private byte[] generateNonce() { byte[] nonce = new byte[12]; - new SecureRandom().nextBytes(nonce); + SECURE_RANDOM.nextBytes(nonce); return nonce; } diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt b/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt index fd4a3f3702..4f8b20f7c6 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/HTTP.kt @@ -5,8 +5,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody -import okhttp3.Response -import java.security.SecureRandom +import org.session.libsignal.utilities.Util.SECURE_RANDOM import java.security.cert.X509Certificate import java.util.concurrent.TimeUnit import javax.net.ssl.SSLContext @@ -35,7 +34,7 @@ object HTTP { override fun getAcceptedIssuers(): Array { return arrayOf() } } val sslContext = SSLContext.getInstance("SSL") - sslContext.init(null, arrayOf( trustManager ), SecureRandom()) + sslContext.init(null, arrayOf( trustManager ), SECURE_RANDOM) OkHttpClient().newBuilder() .sslSocketFactory(sslContext.socketFactory, trustManager) .hostnameVerifier { _, _ -> true } @@ -55,7 +54,7 @@ object HTTP { override fun getAcceptedIssuers(): Array { return arrayOf() } } val sslContext = SSLContext.getInstance("SSL") - sslContext.init(null, arrayOf( trustManager ), SecureRandom()) + sslContext.init(null, arrayOf( trustManager ), SECURE_RANDOM) return OkHttpClient().newBuilder() .sslSocketFactory(sslContext.socketFactory, trustManager) .hostnameVerifier { _, _ -> true } diff --git a/libsignal/src/main/java/org/session/libsignal/utilities/Util.java b/libsignal/src/main/java/org/session/libsignal/utilities/Util.java index 2c9436485f..3b3b7aa5e6 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Util.java +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Util.java @@ -12,12 +12,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.List; public class Util { + public static SecureRandom SECURE_RANDOM = new SecureRandom(); public static byte[] join(byte[]... input) { try { @@ -67,7 +65,7 @@ public class Util { } public static boolean isEmpty(String value) { - return value == null || value.trim().length() == 0; + return value == null || value.trim().isEmpty(); } public static byte[] getSecretBytes(int size) { @@ -80,13 +78,6 @@ public class Util { } } - public static byte[] getRandomLengthBytes(int maxSize) { - SecureRandom secureRandom = new SecureRandom(); - byte[] result = new byte[secureRandom.nextInt(maxSize) + 1]; - secureRandom.nextBytes(result); - return result; - } - public static String readFully(InputStream in) throws IOException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); byte[] buffer = new byte[4096]; @@ -98,7 +89,7 @@ public class Util { in.close(); - return new String(bout.toByteArray()); + return bout.toString(); } public static void readFully(InputStream in, byte[] buffer) throws IOException { @@ -146,9 +137,4 @@ public class Util { } return (int)value; } - - public static List immutableList(T... elements) { - return Collections.unmodifiableList(Arrays.asList(elements.clone())); - } - } From fa0abef243e5301c81ab564ff7076ffeeac18e5d Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Mon, 5 Aug 2024 09:59:02 +0930 Subject: [PATCH 42/45] Fix Snode Version CACHE usage --- .../src/main/java/org/session/libsignal/utilities/Snode.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 58dcbe8a37..288c3fcf1d 100644 --- a/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt +++ b/libsignal/src/main/java/org/session/libsignal/utilities/Snode.kt @@ -42,10 +42,7 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val v private val CACHE = mutableMapOf() @SuppressLint("NotConstructor") - fun Version(value: String) = CACHE.getOrElse(value) { - // internal constructor takes precedence - Snode.Version(value) - } + fun Version(value: String) = CACHE[value] ?: Snode.Version(value).also { CACHE[value] = it } fun Version(parts: List) = Version(parts.joinToString(".")) } From f9ace6a9b92c8df4b0a7346f297c860327b31230 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Mon, 5 Aug 2024 10:26:52 +0930 Subject: [PATCH 43/45] Add LruCache and thread-safety --- .../src/main/java/org/session/libsignal/utilities/Snode.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 288c3fcf1d..675fe55f85 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,7 @@ package org.session.libsignal.utilities import android.annotation.SuppressLint +import android.util.LruCache /** * Create a Snode from a "-" delimited String if valid, null otherwise. @@ -39,10 +40,11 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val v override fun toString(): String = "$address:$port" companion object { - private val CACHE = mutableMapOf() + private val CACHE = LruCache(100) @SuppressLint("NotConstructor") - fun Version(value: String) = CACHE[value] ?: Snode.Version(value).also { CACHE[value] = it } + @Synchronized + fun Version(value: String) = CACHE[value] ?: Snode.Version(value).also { CACHE.put(value, it) } fun Version(parts: List) = Version(parts.joinToString(".")) } From 2960eddd8532d8c2387d7e3ef29f3624b5dca3b7 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Mon, 5 Aug 2024 13:46:55 +0930 Subject: [PATCH 44/45] Fix removeDuplicates --- .../sending_receiving/pollers/Poller.kt | 3 +- .../org/session/libsession/snode/SnodeAPI.kt | 36 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt index 2b7c8159ea..f0caa9d314 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/Poller.kt @@ -142,8 +142,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti val messages = rawMessages["messages"] as? List<*> val processed = if (!messages.isNullOrEmpty()) { SnodeAPI.updateLastMessageHashValueIfPossible(snode, userPublicKey, messages, namespace) - SnodeAPI.removeDuplicates(userPublicKey, messages, namespace, true).mapNotNull { messageBody -> - val rawMessageAsJSON = messageBody as? Map<*, *> ?: return@mapNotNull null + SnodeAPI.removeDuplicates(userPublicKey, messages, namespace, true).mapNotNull { rawMessageAsJSON -> val hashValue = rawMessageAsJSON["hash"] as? String ?: return@mapNotNull null val b64EncodedBody = rawMessageAsJSON["data"] as? String ?: return@mapNotNull null val timestamp = rawMessageAsJSON["t"] as? Long ?: SnodeAPI.nowWithOffset 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 9c584ba0fb..72b144be03 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -660,29 +660,33 @@ object SnodeAPI { } } - fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int, updateStoredHashes: Boolean): List<*> { - val originalMessageHashValues = database.getReceivedMessageHashValues(publicKey, namespace) ?: emptySet() - val receivedMessageHashValues = originalMessageHashValues.toMutableSet() - return rawMessages.filter { rawMessage -> - (rawMessage as? Map<*, *>) - ?.let { it["hash"] as? String } - ?.let { receivedMessageHashValues.add(it) } - ?: false.also { Log.d("Loki", "Missing hash value for message: ${rawMessage?.prettifiedDescription()}.") } + /** + * + * + * TODO Use a db transaction, synchronizing is sufficient for now because + * database#setReceivedMessageHashValues is only called here. + */ + @Synchronized + fun removeDuplicates(publicKey: String, rawMessages: List<*>, namespace: Int, updateStoredHashes: Boolean): List> { + val hashValues = database.getReceivedMessageHashValues(publicKey, namespace)?.toMutableSet() ?: mutableSetOf() + return rawMessages.filterIsInstance>().filter { rawMessage -> + val hash = rawMessage["hash"] as? String + hash ?: Log.d("Loki", "Missing hash value for message: ${rawMessage.prettifiedDescription()}.") + hash?.let(hashValues::add) == true }.also { - if (updateStoredHashes && originalMessageHashValues.containsAll(receivedMessageHashValues)) { - database.setReceivedMessageHashValues(publicKey, receivedMessageHashValues, namespace) + if (updateStoredHashes && it.isNotEmpty()) { + database.setReceivedMessageHashValues(publicKey, hashValues, namespace) } } } - private fun parseEnvelopes(rawMessages: List<*>): List> = rawMessages.mapNotNull { rawMessage -> - val rawMessageAsJSON = rawMessage as? Map<*, *> - val base64EncodedData = rawMessageAsJSON?.get("data") as? String - val data = base64EncodedData?.let { Base64.decode(it) } + private fun parseEnvelopes(rawMessages: List>): List> = rawMessages.mapNotNull { rawMessage -> + val base64EncodedData = rawMessage["data"] as? String + val data = base64EncodedData?.let(Base64::decode) - data ?: Log.d("Loki", "Failed to decode data for message: ${rawMessage?.prettifiedDescription()}.") + data ?: Log.d("Loki", "Failed to decode data for message: ${rawMessage.prettifiedDescription()}.") - data?.runCatching { MessageWrapper.unwrap(this) to rawMessageAsJSON["hash"] as? String } + data?.runCatching { MessageWrapper.unwrap(this) to rawMessage["hash"] as? String } ?.onFailure { Log.d("Loki", "Failed to unwrap data for message: ${rawMessage.prettifiedDescription()}.") } ?.getOrNull() } From 8a9faa182d43322fa418e555461cc355b43280d7 Mon Sep 17 00:00:00 2001 From: bemusementpark Date: Mon, 5 Aug 2024 16:26:20 +0930 Subject: [PATCH 45/45] Fix SnodeAPI error thrown outside of Promise --- .../src/main/java/org/session/libsession/snode/SnodeAPI.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 2f6cf141d1..099b2541ab 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -15,6 +15,7 @@ import nl.komponents.kovenant.all import nl.komponents.kovenant.functional.bind import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.task +import nl.komponents.kovenant.unwrap import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.SodiumUtilities.sodium @@ -202,7 +203,7 @@ object SnodeAPI { } // Public API - fun getAccountID(onsName: String): Promise { + fun getAccountID(onsName: String): Promise = task { val validationCount = 3 val accountIDByteCount = 33 // Hash the ONS name using BLAKE2b @@ -228,7 +229,7 @@ object SnodeAPI { } } } - return all(promises).map { results -> + all(promises).map { results -> results.map { json -> val intermediate = json["result"] as? Map<*, *> ?: throw Error.Generic val hexEncodedCiphertext = intermediate["encrypted_value"] as? String ?: throw Error.Generic @@ -264,7 +265,7 @@ object SnodeAPI { }.takeIf { it.size == validationCount && it.toSet().size == 1 }?.first() ?: throw Error.ValidationFailed } - } + }.unwrap() fun getSwarm(publicKey: String): Promise, Exception> = database.getSwarm(publicKey)?.takeIf { it.size >= minimumSwarmSnodeCount }?.let(Promise.Companion::of)