Merge dev and cleanup

This commit is contained in:
alansley 2024-08-06 11:30:50 +10:00
commit 5cbe289a8d
59 changed files with 1027 additions and 996 deletions

View File

@ -28,8 +28,8 @@ configurations.all {
exclude module: "commons-logging" exclude module: "commons-logging"
} }
def canonicalVersionCode = 376 def canonicalVersionCode = 377
def canonicalVersionName = "1.18.6" def canonicalVersionName = "1.19.0"
def postFixSize = 10 def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1, def abiPostFix = ['armeabi-v7a' : 1,

View File

@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.sskenvironment.ProfileManager;
import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager; import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager;
import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository; import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository;
import org.thoughtcrime.securesms.util.Broadcaster; import org.thoughtcrime.securesms.util.Broadcaster;
import org.thoughtcrime.securesms.util.VersionDataFetcher;
import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper; import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper;
import org.thoughtcrime.securesms.webrtc.CallMessageProcessor; import org.thoughtcrime.securesms.webrtc.CallMessageProcessor;
import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory;
@ -110,7 +111,6 @@ import javax.inject.Inject;
import dagger.hilt.EntryPoints; import dagger.hilt.EntryPoints;
import dagger.hilt.android.HiltAndroidApp; import dagger.hilt.android.HiltAndroidApp;
import kotlin.Unit; import kotlin.Unit;
import kotlinx.coroutines.Job;
import network.loki.messenger.BuildConfig; import network.loki.messenger.BuildConfig;
import network.loki.messenger.libsession_util.ConfigBase; import network.loki.messenger.libsession_util.ConfigBase;
import network.loki.messenger.libsession_util.UserProfile; import network.loki.messenger.libsession_util.UserProfile;
@ -151,6 +151,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
@Inject PushRegistry pushRegistry; @Inject PushRegistry pushRegistry;
@Inject ConfigFactory configFactory; @Inject ConfigFactory configFactory;
@Inject LastSentTimestampCache lastSentTimestampCache; @Inject LastSentTimestampCache lastSentTimestampCache;
@Inject VersionDataFetcher versionDataFetcher;
CallMessageProcessor callMessageProcessor; CallMessageProcessor callMessageProcessor;
MessagingModuleConfiguration messagingModuleConfiguration; MessagingModuleConfiguration messagingModuleConfiguration;
@ -275,6 +276,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
OpenGroupManager.INSTANCE.startPolling(); OpenGroupManager.INSTANCE.startPolling();
}); });
// fetch last version data
versionDataFetcher.startTimedVersionCheck();
} }
@Override @Override
@ -287,12 +291,14 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
poller.stopIfNeeded(); poller.stopIfNeeded();
} }
ClosedGroupPollerV2.getShared().stopAll(); ClosedGroupPollerV2.getShared().stopAll();
versionDataFetcher.stopTimedVersionCheck();
} }
@Override @Override
public void onTerminate() { public void onTerminate() {
stopKovenant(); // Loki stopKovenant(); // Loki
OpenGroupManager.INSTANCE.stopPolling(); OpenGroupManager.INSTANCE.stopPolling();
versionDataFetcher.stopTimedVersionCheck();
super.onTerminate(); super.onTerminate();
} }

View File

@ -7,8 +7,6 @@ import com.squareup.phrase.Phrase
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.LocalisedTimeUtil import org.session.libsession.LocalisedTimeUtil
import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.TIME_LARGE_KEY
import org.session.libsignal.utilities.Log
import java.time.Duration
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds

View File

@ -27,7 +27,11 @@ import org.thoughtcrime.securesms.groups.JoinCommunityFragment
@AndroidEntryPoint @AndroidEntryPoint
class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate { 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( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,

View File

@ -1,10 +1,13 @@
package org.thoughtcrime.securesms.conversation.start.newmessage 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.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter 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.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import network.loki.messenger.R 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.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.AppBar
import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode 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.SessionOutlinedTextField
import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.components.SessionTabRow
import org.thoughtcrime.securesms.ui.contentDescription 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.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.accountIdEnter, R.string.qrScan) private val TITLES = listOf(R.string.accountIdEnter, R.string.qrScan)
@ -76,63 +93,127 @@ private fun EnterAccountId(
callbacks: Callbacks, callbacks: Callbacks,
onHelp: () -> Unit = {} onHelp: () -> Unit = {}
) { ) {
Column( // the scaffold is required to provide the contentPadding. That contentPadding is needed
modifier = Modifier // to properly handle the ime padding.
.fillMaxSize() Scaffold() { contentPadding ->
.verticalScroll(rememberScrollState()) // we need this extra surface to handle nested scrolling properly,
.imePadding() // because this scrollable component is inside a bottomSheet dialog which is itself scrollable
) { Surface(
Column( modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()),
modifier = Modifier.padding(vertical = LocalDimensions.current.spacing), color = LocalColors.current.backgroundSecondary
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
)
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) var accountModifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
BorderlessButtonWithIcon( //<<<<<<< HEAD
text = stringResource(R.string.messageNewDescriptionMobile), // BorderlessButtonWithIcon(
modifier = Modifier // text = stringResource(R.string.messageNewDescriptionMobile),
.contentDescription(R.string.AccessibilityId_help_desk_link) // modifier = Modifier
.padding(horizontal = LocalDimensions.current.mediumSpacing) // .contentDescription(R.string.AccessibilityId_help_desk_link)
.fillMaxWidth(), // .padding(horizontal = LocalDimensions.current.mediumSpacing)
style = LocalType.current.small, // .fillMaxWidth(),
color = LocalColors.current.textSecondary, // style = LocalType.current.small,
iconRes = R.drawable.ic_circle_question_mark, // color = LocalColors.current.textSecondary,
onClick = onHelp // iconRes = R.drawable.ic_circle_question_mark,
) // onClick = onHelp
} // )
// }
//=======
// There is a known issue with the ime padding on android versions below 30
/// 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()
accountModifier = accountModifier.padding(bottom = keyboardHeight)
} else {
accountModifier = accountModifier
.consumeWindowInsets(contentPadding)
.imePadding()
}
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) Column(
Spacer(Modifier.weight(2f)) 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( Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
modifier = Modifier
.align(Alignment.CenterHorizontally) BorderlessButtonWithIcon(
.padding(horizontal = LocalDimensions.current.xlargeSpacing) text = stringResource(R.string.messageNewDescriptionMobile),
.padding(bottom = LocalDimensions.current.smallSpacing) modifier = Modifier
.fillMaxWidth() .contentDescription(R.string.AccessibilityId_help_desk_link)
.contentDescription(R.string.next), .padding(horizontal = LocalDimensions.current.mediumSpacing)
enabled = state.isNextButtonEnabled, .fillMaxWidth(),
onClick = callbacks::onContinue style = LocalType.current.small,
) { color = LocalColors.current.textSecondary,
LoadingArcOr(state.loading) { iconRes = R.drawable.ic_circle_question_mark,
Text(stringResource(R.string.next)) 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<Dp> {
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 @Preview
@Composable @Composable
private fun PreviewNewMessage( private fun PreviewNewMessage(

View File

@ -46,7 +46,7 @@ internal class NewMessageViewModel @Inject constructor(
} }
override fun onContinue() { override fun onContinue() {
val idOrONS = state.value.newMessageIdOrOns val idOrONS = state.value.newMessageIdOrOns.trim()
if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) { if (PublicKeyValidation.isValid(idOrONS, isPrefixRequired = false)) {
onUnvalidatedPublicKey(publicKey = idOrONS) onUnvalidatedPublicKey(publicKey = idOrONS)

View File

@ -178,7 +178,6 @@ import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
import org.thoughtcrime.securesms.showSessionDialog import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@ -190,7 +189,6 @@ import org.thoughtcrime.securesms.util.isScrolledToBottom
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.start
import org.thoughtcrime.securesms.util.toPx import org.thoughtcrime.securesms.util.toPx
private const val TAG = "ConversationActivityV2" private const val TAG = "ConversationActivityV2"
@ -1651,8 +1649,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val text = getMessageBody() val text = getMessageBody()
val userPublicKey = textSecurePreferences.getLocalNumber() val userPublicKey = textSecurePreferences.getLocalNumber()
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey) val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) { if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) {
start<RecoveryPasswordActivity>() showSessionDialog {
title(R.string.warning)
text(R.string.recoveryPasswordWarningSendDescription)
button(R.string.send) { sendTextOnlyMessage(true) }
cancelButton()
}
return null
} }
// Create the message // Create the message
val message = VisibleMessage().applyExpiryMode(viewModel.threadId) val message = VisibleMessage().applyExpiryMode(viewModel.threadId)

View File

@ -36,7 +36,6 @@ import com.google.android.mms.pdu_alt.EncodedStringValue
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.security.SecureRandom
import java.util.Collections import java.util.Collections
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.max import kotlin.math.max
@ -247,32 +246,6 @@ object Util {
return result 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 <T> getRandomElement(elements: Array<T>): T {
return elements[SecureRandom().nextInt(elements.size)]
}
fun <T> getRandomElement(elements: List<T>): 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? { fun uri(uri: String?): Uri? {
return if (uri == null) null return if (uri == null) null
else Uri.parse(uri) else Uri.parse(uri)

View File

@ -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.warning)
text(R.string.recoveryPasswordWarningSendDescription)
button(R.string.send) { send() }
cancelButton()
}
private fun send() {
proceed?.invoke()
dismiss()
}
}

View File

@ -1,14 +1,14 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import java.security.SecureRandom;
/** /**
* A provider that is responsible for creating or retrieving the AttachmentSecret model. * 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) { private AttachmentSecret createAndStoreAttachmentSecret(@NonNull Context context) {
SecureRandom random = new SecureRandom();
byte[] secret = new byte[32]; byte[] secret = new byte[32];
random.nextBytes(secret); SECURE_RANDOM.nextBytes(secret);
AttachmentSecret attachmentSecret = new AttachmentSecret(null, null, secret); AttachmentSecret attachmentSecret = new AttachmentSecret(null, null, secret);
storeAttachmentSecret(context, attachmentSecret); storeAttachmentSecret(context, attachmentSecret);

View File

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -8,7 +10,6 @@ import androidx.annotation.NonNull;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom;
public class DatabaseSecretProvider { public class DatabaseSecretProvider {
@ -60,9 +61,8 @@ public class DatabaseSecretProvider {
} }
private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) { private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
SecureRandom random = new SecureRandom();
byte[] secret = new byte[32]; byte[] secret = new byte[32];
random.nextBytes(secret); SECURE_RANDOM.nextBytes(secret);
DatabaseSecret databaseSecret = new DatabaseSecret(secret); DatabaseSecret databaseSecret = new DatabaseSecret(secret);

View File

@ -1,6 +1,8 @@
package org.thoughtcrime.securesms.crypto; package org.thoughtcrime.securesms.crypto;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import android.util.Pair; import android.util.Pair;
@ -11,7 +13,6 @@ import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream; import javax.crypto.CipherOutputStream;
@ -31,7 +32,7 @@ public class ModernEncryptingPartOutputStream {
throws IOException throws IOException
{ {
byte[] random = new byte[32]; byte[] random = new byte[32];
new SecureRandom().nextBytes(random); SECURE_RANDOM.nextBytes(random);
try { try {
Mac mac = Mac.getInstance("HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256");

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.database; package org.thoughtcrime.securesms.database;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
@ -26,7 +28,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.BitmapUtil;
import java.io.Closeable; import java.io.Closeable;
import java.security.SecureRandom;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -303,7 +304,7 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
public void updateProfilePicture(String groupID, byte[] newValue) { public void updateProfilePicture(String groupID, byte[] newValue) {
long avatarId; long avatarId;
if (newValue != null) avatarId = Math.abs(new SecureRandom().nextLong()); if (newValue != null) avatarId = Math.abs(SECURE_RANDOM.nextLong());
else avatarId = 0; else avatarId = 0;
@ -458,12 +459,6 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId}); 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) { public boolean hasGroup(@NonNull String groupId) {
try (Cursor cursor = databaseHelper.getReadableDatabase().rawQuery( try (Cursor cursor = databaseHelper.getReadableDatabase().rawQuery(
"SELECT 1 FROM " + TABLE_NAME + " WHERE " + GROUP_ID + " = ? LIMIT 1", "SELECT 1 FROM " + TABLE_NAME + " WHERE " + GROUP_ID + " = ? LIMIT 1",

View File

@ -166,8 +166,6 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
const val RESET_SEQ_NO = "UPDATE $lastMessageServerIDTable SET $lastMessageServerID = 0;" const val RESET_SEQ_NO = "UPDATE $lastMessageServerIDTable SET $lastMessageServerID = 0;"
const val EMPTY_VERSION = "0.0.0"
// endregion // endregion
} }
@ -175,15 +173,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
return database.get(snodePoolTable, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor -> return database.get(snodePoolTable, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor ->
val snodePoolAsString = cursor.getString(cursor.getColumnIndexOrThrow(snodePool)) val snodePoolAsString = cursor.getString(cursor.getColumnIndexOrThrow(snodePool))
snodePoolAsString.split(", ").mapNotNull { snodeAsString -> snodePoolAsString.split(", ").mapNotNull(::Snode)
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)
}
}?.toSet() ?: setOf() }?.toSet() ?: setOf()
} }
@ -231,18 +221,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
fun get(indexPath: String): Snode? { fun get(indexPath: String): Snode? {
return database.get(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor -> return database.get(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor ->
val snodeAsString = cursor.getString(cursor.getColumnIndexOrThrow(snode)) Snode(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
}
} }
} }
val result = mutableListOf<List<Snode>>() val result = mutableListOf<List<Snode>>()
@ -276,15 +255,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val database = databaseHelper.readableDatabase val database = databaseHelper.readableDatabase
return database.get(swarmTable, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor -> return database.get(swarmTable, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor ->
val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm)) val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm))
swarmAsString.split(", ").mapNotNull { targetAsString -> swarmAsString.split(", ").mapNotNull(::Snode)
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)
}
}?.toSet() }?.toSet()
} }

View File

@ -46,11 +46,11 @@ import org.session.libsession.utilities.IdentityKeyMismatchList
import org.session.libsession.utilities.NetworkFailure import org.session.libsession.utilities.NetworkFailure
import org.session.libsession.utilities.NetworkFailureList import org.session.libsession.utilities.NetworkFailureList
import org.session.libsession.utilities.TextSecurePreferences.Companion.isReadReceiptsEnabled 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.libsession.utilities.recipients.Recipient
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.ThreadUtils.queue import org.session.libsignal.utilities.ThreadUtils.queue
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment
import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener
@ -66,7 +66,6 @@ import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.util.asSequence import org.thoughtcrime.securesms.util.asSequence
import java.io.Closeable import java.io.Closeable
import java.io.IOException import java.io.IOException
import java.security.SecureRandom
import java.util.LinkedList import java.util.LinkedList
class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : MessagingDatabase(context, databaseHelper) { 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?, inner class OutgoingMessageReader(private val message: OutgoingMediaMessage?,
private val threadId: Long) { private val threadId: Long) {
private val id = SecureRandom().nextLong() private val id = SECURE_RANDOM.nextLong()
val current: MessageRecord val current: MessageRecord
get() { get() {
val slideDeck = SlideDeck(context, message!!.attachments) val slideDeck = SlideDeck(context, message!!.attachments)

View File

@ -17,6 +17,8 @@
*/ */
package org.thoughtcrime.securesms.database; package org.thoughtcrime.securesms.database;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
@ -49,7 +51,6 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
@ -784,7 +785,7 @@ public class SmsDatabase extends MessagingDatabase {
public OutgoingMessageReader(OutgoingTextMessage message, long threadId) { public OutgoingMessageReader(OutgoingTextMessage message, long threadId) {
this.message = message; this.message = message;
this.threadId = threadId; this.threadId = threadId;
this.id = new SecureRandom().nextLong(); this.id = SECURE_RANDOM.nextLong();
} }
public MessageRecord getCurrent() { public MessageRecord getCurrent() {

View File

@ -1,9 +1,10 @@
package org.thoughtcrime.securesms.glide; package org.thoughtcrime.securesms.glide;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom;
import okhttp3.Headers; import okhttp3.Headers;
import okhttp3.Interceptor; import okhttp3.Interceptor;
@ -30,15 +31,15 @@ public class PaddedHeadersInterceptor implements Interceptor {
private @NonNull Headers getPaddedHeaders(@NonNull Headers headers) { private @NonNull Headers getPaddedHeaders(@NonNull Headers headers) {
return headers.newBuilder() 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(); .build();
} }
private static @NonNull String getRandomString(@NonNull SecureRandom secureRandom, int minLength, int maxLength) { private static @NonNull String getRandomString(int minLength, int maxLength) {
char[] buffer = new char[secureRandom.nextInt(maxLength - minLength) + minLength]; char[] buffer = new char[SECURE_RANDOM.nextInt(maxLength - minLength) + minLength];
for (int i = 0 ; i < buffer.length; i++) { 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); return new String(buffer);

View File

@ -89,6 +89,9 @@ import java.util.Calendar
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
private const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
private const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
@AndroidEntryPoint @AndroidEntryPoint
class HomeActivity : PassphraseRequiredActionBarActivity(), class HomeActivity : PassphraseRequiredActionBarActivity(),
ConversationClickListener, ConversationClickListener,
@ -96,10 +99,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private val TAG = "HomeActivity" private val TAG = "HomeActivity"
companion object { // companion object {
const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT" // const val NEW_ACCOUNT = "HomeActivity_NEW_ACCOUNT"
const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING" // const val FROM_ONBOARDING = "HomeActivity_FROM_ONBOARDING"
} // }
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private lateinit var glide: RequestManager private lateinit var glide: RequestManager
@ -148,7 +151,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 // region Lifecycle
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) { override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
@ -262,7 +266,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
else -> buildList { else -> buildList {
result.contactAndGroupList.takeUnless { it.isEmpty() }?.let { result.contactAndGroupList.takeUnless { it.isEmpty() }?.let {
add(GlobalSearchAdapter.Model.Header(R.string.contactContacts)) add(GlobalSearchAdapter.Model.Header(R.string.sessionConversations))
addAll(it) addAll(it)
} }
result.messageResults.takeUnless { it.isEmpty() }?.let { result.messageResults.takeUnless { it.isEmpty() }?.let {
@ -275,8 +279,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
} }
EventBus.getDefault().register(this@HomeActivity) EventBus.getDefault().register(this@HomeActivity)
if (intent.hasExtra(FROM_ONBOARDING) if (isFromOnboarding) {
&& intent.getBooleanExtra(FROM_ONBOARDING, false)) {
if (Build.VERSION.SDK_INT >= 33 && if (Build.VERSION.SDK_INT >= 33 &&
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) { (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled().not()) {
Permissions.with(this) Permissions.with(this)
@ -681,10 +684,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
} }
} }
fun Context.startHomeActivity(isNewAccount: Boolean) { fun Context.startHomeActivity(isFromOnboarding: Boolean, isNewAccount: Boolean) {
Intent(this, HomeActivity::class.java).apply { Intent(this, HomeActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(HomeActivity.NEW_ACCOUNT, true) putExtra(NEW_ACCOUNT, isNewAccount)
putExtra(HomeActivity.FROM_ONBOARDING, true) putExtra(FROM_ONBOARDING, isFromOnboarding)
}.also(::startActivity) }.also(::startActivity)
} }

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.logging; package org.thoughtcrime.securesms.logging;
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -17,7 +18,6 @@ import java.io.IOException;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
@ -64,7 +64,7 @@ class LogFile {
} }
void writeEntry(@NonNull String entry) throws IOException { void writeEntry(@NonNull String entry) throws IOException {
new SecureRandom().nextBytes(ivBuffer); SECURE_RANDOM.nextBytes(ivBuffer);
byte[] plaintext = entry.getBytes(); byte[] plaintext = entry.getBytes();
try { try {

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.logging; package org.thoughtcrime.securesms.logging;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -9,7 +11,6 @@ import org.session.libsignal.utilities.Base64;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom;
class LogSecretProvider { class LogSecretProvider {
@ -40,9 +41,8 @@ class LogSecretProvider {
} }
private static byte[] createAndStoreSecret(@NonNull Context context) { private static byte[] createAndStoreSecret(@NonNull Context context) {
SecureRandom random = new SecureRandom();
byte[] secret = new byte[32]; byte[] secret = new byte[32];
random.nextBytes(secret); SECURE_RANDOM.nextBytes(secret);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(secret); KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(secret);

View File

@ -21,7 +21,6 @@ import android.content.res.Resources
import android.net.Uri import android.net.Uri
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import com.squareup.phrase.Phrase import com.squareup.phrase.Phrase
import java.security.SecureRandom
import network.loki.messenger.R import network.loki.messenger.R
import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.attachments.Attachment
import org.session.libsession.messaging.sending_receiving.attachments.AttachmentTransferProgress 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.StringSubstitutionConstants.EMOJI_KEY
import org.session.libsession.utilities.Util.equals import org.session.libsession.utilities.Util.equals
import org.session.libsession.utilities.Util.hashCode import org.session.libsession.utilities.Util.hashCode
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import org.session.libsignal.utilities.guava.Optional import org.session.libsignal.utilities.guava.Optional
import org.thoughtcrime.securesms.conversation.v2.Util import org.thoughtcrime.securesms.conversation.v2.Util
import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil
@ -160,7 +160,7 @@ abstract class Slide(@JvmField protected val context: Context, protected val att
): Attachment { ): Attachment {
val resolvedType = val resolvedType =
Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime) Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime)
val fastPreflightId = SecureRandom().nextLong().toString() val fastPreflightId = SECURE_RANDOM.nextLong().toString()
return UriAttachment( return UriAttachment(
uri, uri,
if (hasThumbnail) uri else null, if (hasThumbnail) uri else null,

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.net; package org.thoughtcrime.securesms.net;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
@ -15,7 +17,6 @@ import org.session.libsignal.utilities.guava.Optional;
import java.io.FilterInputStream; import java.io.FilterInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
@ -54,7 +55,7 @@ public class ChunkedDataFetcher {
private RequestController fetchChunksWithUnknownTotalSize(@NonNull String url, @NonNull Callback callback) { private RequestController fetchChunksWithUnknownTotalSize(@NonNull String url, @NonNull Callback callback) {
CompositeRequestController compositeController = new CompositeRequestController(); CompositeRequestController compositeController = new CompositeRequestController();
long chunkSize = new SecureRandom().nextInt(1024) + 1024; long chunkSize = SECURE_RANDOM.nextInt(1024) + 1024;
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
.cacheControl(NO_CACHE) .cacheControl(NO_CACHE)

View File

@ -32,7 +32,7 @@ class LoadingActivity: BaseActionBarActivity() {
when { when {
loadFailed -> startPickDisplayNameActivity(loadFailed = true) loadFailed -> startPickDisplayNameActivity(loadFailed = true)
else -> startHomeActivity(isNewAccount = false) else -> startHomeActivity(isNewAccount = false, isFromOnboarding = true)
} }
finish() finish()

View File

@ -8,6 +8,7 @@ import org.session.libsignal.utilities.KeyHelper
import org.session.libsignal.utilities.hexEncodedPublicKey import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.VersionDataFetcher
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -16,6 +17,7 @@ class CreateAccountManager @Inject constructor(
private val application: Application, private val application: Application,
private val prefs: TextSecurePreferences, private val prefs: TextSecurePreferences,
private val configFactory: ConfigFactory, private val configFactory: ConfigFactory,
private val versionDataFetcher: VersionDataFetcher
) { ) {
private val database: LokiAPIDatabaseProtocol private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage get() = SnodeModule.shared.storage
@ -41,5 +43,7 @@ class CreateAccountManager @Inject constructor(
prefs.setLocalRegistrationId(registrationID) prefs.setLocalRegistrationId(registrationID)
prefs.setLocalNumber(userHexEncodedPublicKey) prefs.setLocalNumber(userHexEncodedPublicKey)
prefs.setRestorationTime(0) prefs.setRestorationTime(0)
versionDataFetcher.startTimedVersionCheck()
} }
} }

View File

@ -12,6 +12,7 @@ import org.session.libsignal.utilities.hexEncodedPublicKey
import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.KeyPairUtilities
import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.util.VersionDataFetcher
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -19,7 +20,8 @@ import javax.inject.Singleton
class LoadAccountManager @Inject constructor( class LoadAccountManager @Inject constructor(
@dagger.hilt.android.qualifiers.ApplicationContext private val context: Context, @dagger.hilt.android.qualifiers.ApplicationContext private val context: Context,
private val configFactory: ConfigFactory, private val configFactory: ConfigFactory,
private val prefs: TextSecurePreferences private val prefs: TextSecurePreferences,
private val versionDataFetcher: VersionDataFetcher
) { ) {
private val database: LokiAPIDatabaseProtocol private val database: LokiAPIDatabaseProtocol
get() = SnodeModule.shared.storage get() = SnodeModule.shared.storage
@ -52,6 +54,8 @@ class LoadAccountManager @Inject constructor(
setHasViewedSeed(true) setHasViewedSeed(true)
} }
versionDataFetcher.startTimedVersionCheck()
ApplicationContext.getInstance(context).retrieveUserProfile() ApplicationContext.getInstance(context).retrieveUserProfile()
} }
} }

View File

@ -49,7 +49,7 @@ class MessageNotificationsActivity : BaseActionBarActivity() {
viewModel.events.collect { viewModel.events.collect {
when (it) { when (it) {
Event.Loading -> start<LoadingActivity>() Event.Loading -> start<LoadingActivity>()
Event.OnboardingComplete -> startHomeActivity(isNewAccount = true) Event.OnboardingComplete -> startHomeActivity(isNewAccount = true, isFromOnboarding = true)
} }
} }
} }

View File

@ -45,7 +45,7 @@ class PickDisplayNameActivity : BaseActionBarActivity() {
viewModel.events.collect { viewModel.events.collect {
when (it) { when (it) {
is Event.CreateAccount -> startMessageNotificationsActivity(it.profileName) is Event.CreateAccount -> startMessageNotificationsActivity(it.profileName)
Event.LoadAccountComplete -> startHomeActivity(isNewAccount = false) Event.LoadAccountComplete -> startHomeActivity(isNewAccount = false, isFromOnboarding = true)
} }
} }
} }

View File

@ -1,9 +1,9 @@
package org.thoughtcrime.securesms.permissions; package org.thoughtcrime.securesms.permissions;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
@ -11,9 +11,7 @@ import android.os.Build;
import android.provider.Settings; import android.provider.Settings;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.view.Display; import android.view.Display;
import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Button;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -28,13 +26,10 @@ import org.session.libsession.utilities.ServiceUtil;
import org.thoughtcrime.securesms.util.LRUCache; import org.thoughtcrime.securesms.util.LRUCache;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.security.SecureRandom;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import network.loki.messenger.R;
public class Permissions { public class Permissions {
private static final Map<Integer, PermissionsRequest> OUTSTANDING = new LRUCache<>(2); private static final Map<Integer, PermissionsRequest> OUTSTANDING = new LRUCache<>(2);
@ -172,7 +167,7 @@ public class Permissions {
} }
private void executePermissionsRequest(PermissionsRequest request) { private void executePermissionsRequest(PermissionsRequest request) {
int requestCode = new SecureRandom().nextInt(65434) + 100; int requestCode = SECURE_RANDOM.nextInt(65434) + 100;
synchronized (OUTSTANDING) { synchronized (OUTSTANDING) {
OUTSTANDING.put(requestCode, request); OUTSTANDING.put(requestCode, request);

View File

@ -37,7 +37,6 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.squareup.phrase.Phrase import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.io.File import java.io.File
import java.security.SecureRandom
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
@ -67,6 +66,7 @@ import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.truncateIdForDisplay import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.avatar.AvatarSelection
import org.thoughtcrime.securesms.components.ProfilePictureView import org.thoughtcrime.securesms.components.ProfilePictureView
@ -300,7 +300,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
val userConfig = configFactory.user val userConfig = configFactory.user
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture) AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
prefs.setProfileAvatarId(SecureRandom().nextInt() ) prefs.setProfileAvatarId(SECURE_RANDOM.nextInt() )
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey) ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
// Attempt to grab the details we require to update the profile picture // Attempt to grab the details we require to update the profile picture

View File

@ -6,7 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.ui.theme.selectedTheme import org.thoughtcrime.securesms.ui.theme.invalidateComposeThemeColors
import org.thoughtcrime.securesms.util.ThemeState import org.thoughtcrime.securesms.util.ThemeState
import org.thoughtcrime.securesms.util.themeState import org.thoughtcrime.securesms.util.themeState
import javax.inject.Inject import javax.inject.Inject
@ -21,6 +21,8 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec
prefs.setAccentColorStyle(newAccentColorStyle) prefs.setAccentColorStyle(newAccentColorStyle)
// update UI state // update UI state
_uiState.value = prefs.themeState() _uiState.value = prefs.themeState()
invalidateComposeThemeColors()
} }
fun setNewStyle(newThemeStyle: String) { fun setNewStyle(newThemeStyle: String) {
@ -28,16 +30,13 @@ class AppearanceSettingsViewModel @Inject constructor(private val prefs: TextSec
// update UI state // update UI state
_uiState.value = prefs.themeState() _uiState.value = prefs.themeState()
// force compose to refresh its style reference invalidateComposeThemeColors()
selectedTheme = null
} }
fun setNewFollowSystemSettings(followSystemSettings: Boolean) { fun setNewFollowSystemSettings(followSystemSettings: Boolean) {
prefs.setFollowSystemSettings(followSystemSettings) prefs.setFollowSystemSettings(followSystemSettings)
_uiState.value = prefs.themeState() _uiState.value = prefs.themeState()
// force compose to refresh its style reference invalidateComposeThemeColors()
selectedTheme = null
} }
}
}

View File

@ -1,10 +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 light: ThemeColors,
val dark: ThemeColors
)

View File

@ -0,0 +1,20 @@
package org.thoughtcrime.securesms.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
fun interface ThemeColorsProvider {
@Composable
fun get(): ThemeColors
}
@Suppress("FunctionName")
fun FollowSystemThemeColorsProvider(light: ThemeColors, dark: ThemeColors) = ThemeColorsProvider {
when {
isSystemInDarkTheme() -> dark
else -> light
}
}
@Suppress("FunctionName")
fun ThemeColorsProvider(colors: ThemeColors) = ThemeColorsProvider { colors }

View File

@ -1,7 +1,5 @@
package org.thoughtcrime.securesms.ui.theme package org.thoughtcrime.securesms.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences.Companion.BLUE_ACCENT import org.session.libsession.utilities.TextSecurePreferences.Companion.BLUE_ACCENT
@ -17,38 +15,25 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.YELLOW_A
* Some behaviour is hardcoded to cater for legacy usage of people with themes already set * 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 * But future themes will be picked and set directly from the "Appearance" screen
*/ */
@Composable fun TextSecurePreferences.getColorsProvider(): ThemeColorsProvider {
fun TextSecurePreferences.getComposeTheme(): ThemeColors {
val selectedTheme = getThemeStyle() val selectedTheme = getThemeStyle()
// get the chosen primary color from the preferences // get the chosen primary color from the preferences
val selectedPrimary = primaryColor() val selectedPrimary = primaryColor()
// create a theme set with the appropriate primary val isOcean = "ocean" in selectedTheme
val colorSet = when(selectedTheme){
TextSecurePreferences.OCEAN_DARK, val createLight = if (isOcean) ::OceanLight else ::ClassicLight
TextSecurePreferences.OCEAN_LIGHT -> ThemeColorSet( val createDark = if (isOcean) ::OceanDark else ::ClassicDark
light = OceanLight(selectedPrimary),
dark = OceanDark(selectedPrimary) return when {
) getFollowSystemSettings() -> FollowSystemThemeColorsProvider(
light = createLight(selectedPrimary),
else -> ThemeColorSet( dark = createDark(selectedPrimary)
light = ClassicLight(selectedPrimary),
dark = ClassicDark(selectedPrimary)
) )
"light" in selectedTheme -> ThemeColorsProvider(createLight(selectedPrimary))
else -> ThemeColorsProvider(createDark(selectedPrimary))
} }
// 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()) { fun TextSecurePreferences.primaryColor(): Color = when(getSelectedAccentColor()) {
@ -60,6 +45,3 @@ fun TextSecurePreferences.primaryColor(): Color = when(getSelectedAccentColor())
YELLOW_ACCENT -> primaryYellow YELLOW_ACCENT -> primaryYellow
else -> primaryGreen else -> primaryGreen
} }

View File

@ -20,7 +20,12 @@ import org.session.libsession.utilities.AppTextSecurePreferences
val LocalColors = compositionLocalOf <ThemeColors> { ClassicDark() } val LocalColors = compositionLocalOf <ThemeColors> { ClassicDark() }
val LocalType = compositionLocalOf { sessionTypography } val LocalType = compositionLocalOf { sessionTypography }
var selectedTheme: ThemeColors? = null var cachedColorsProvider: ThemeColorsProvider? = null
fun invalidateComposeThemeColors() {
// invalidate compose theme colors
cachedColorsProvider = null
}
/** /**
* Apply a Material2 compose theme based on user selections in SharedPreferences. * Apply a Material2 compose theme based on user selections in SharedPreferences.
@ -29,15 +34,15 @@ var selectedTheme: ThemeColors? = null
fun SessionMaterialTheme( fun SessionMaterialTheme(
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
// set the theme data if it hasn't been done yet val context = LocalContext.current
if(selectedTheme == null) { val preferences = AppTextSecurePreferences(context)
// 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()
}
SessionMaterialTheme(colors = selectedTheme ?: ClassicDark()) { content() } val cachedColors = cachedColorsProvider ?: preferences.getColorsProvider().also { cachedColorsProvider = it }
SessionMaterialTheme(
colors = cachedColors.get(),
content = content
)
} }
/** /**
@ -58,9 +63,8 @@ fun SessionMaterialTheme(
LocalType provides sessionTypography, LocalType provides sessionTypography,
LocalContentColor provides colors.text, LocalContentColor provides colors.text,
LocalTextSelectionColors provides colors.textSelectionColors, LocalTextSelectionColors provides colors.textSelectionColors,
) { content = content
content() )
}
} }
} }

View File

@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.util
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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 javax.inject.Inject
import javax.inject.Singleton
import kotlin.time.Duration.Companion.hours
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 handler = Handler(Looper.getMainLooper())
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)
}
}
}
private val scope = CoroutineScope(Dispatchers.Default)
/**
* Schedules fetching version data.
*
* @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(
delayMillis: Long = REFRESH_TIME_MS + prefs.getLastVersionCheck() - System.currentTimeMillis()
) {
stopTimedVersionCheck()
handler.postDelayed(fetchVersionData, delayMillis)
}
fun stopTimedVersionCheck() {
handler.removeCallbacks(fetchVersionData)
}
}

View File

@ -1,8 +1,10 @@
package org.thoughtcrime.securesms.webrtc package org.thoughtcrime.securesms.webrtc
import android.content.Context import android.content.Context
import org.session.libsignal.crypto.shuffledRandom
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.SettableFuture 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.Camera
import org.thoughtcrime.securesms.webrtc.video.CameraEventListener import org.thoughtcrime.securesms.webrtc.video.CameraEventListener
import org.thoughtcrime.securesms.webrtc.video.CameraState import org.thoughtcrime.securesms.webrtc.video.CameraState
@ -22,9 +24,7 @@ import org.webrtc.SurfaceTextureHelper
import org.webrtc.VideoSink import org.webrtc.VideoSink
import org.webrtc.VideoSource import org.webrtc.VideoSource
import org.webrtc.VideoTrack import org.webrtc.VideoTrack
import java.security.SecureRandom
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import kotlin.random.asKotlinRandom
class PeerConnectionWrapper(private val context: Context, class PeerConnectionWrapper(private val context: Context,
private val factory: PeerConnectionFactory, private val factory: PeerConnectionFactory,
@ -49,8 +49,7 @@ class PeerConnectionWrapper(private val context: Context,
private var isInitiator = false private var isInitiator = false
private fun initPeerConnection() { private fun initPeerConnection() {
val random = SecureRandom().asKotlinRandom() val iceServers = listOf("freyr","angus","hereford","holstein", "brahman").shuffledRandom().take(2).map { sub ->
val iceServers = listOf("freyr","angus","hereford","holstein", "brahman").shuffled(random).take(2).map { sub ->
PeerConnection.IceServer.builder("turn:$sub.getsession.org") PeerConnection.IceServer.builder("turn:$sub.getsession.org")
.setUsername("session202111") .setUsername("session202111")
.setPassword("053c268164bc7bd7") .setPassword("053c268164bc7bd7")

View File

@ -0,0 +1,52 @@
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<Array<Any>> = 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),
// 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),
)
}
@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))
}
}

@ -1 +1 @@
Subproject commit 626b6628a2af8fff798042416b3b469b8bfc6ecf Subproject commit 0193c36e0dad461385d6407a00f33b7314e6d740

View File

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

View File

@ -0,0 +1,34 @@
#include <jni.h>
#include <session/blinding.hpp>
#include "util.h"
#include "jni_utils.h"
//
// Created by Thomas Ruffie on 29/7/2024.
//
extern "C"
JNIEXPORT jobject JNICALL
Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionKeyPair(JNIEnv *env,
jobject thiz,
jbyteArray ed25519_secret_key) {
return jni_utils::run_catching_cxx_exception_or_throws<jobject>(env, [=] {
const auto [pk, sk] = session::blind_version_key_pair(util::ustring_from_bytes(env, ed25519_secret_key));
jclass kp_class = env->FindClass("network/loki/messenger/libsession_util/util/KeyPair");
jmethodID kp_constructor = env->GetMethodID(kp_class, "<init>", "([B[B)V");
return env->NewObject(kp_class, kp_constructor, util::bytes_from_ustring(env, {pk.data(), pk.size()}), util::bytes_from_ustring(env, {sk.data(), sk.size()}));
});
}
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_network_loki_messenger_libsession_1util_util_BlindKeyAPI_blindVersionSign(JNIEnv *env,
jobject thiz,
jbyteArray ed25519_secret_key,
jlong timestamp) {
return jni_utils::run_catching_cxx_exception_or_throws<jbyteArray>(env, [=] {
auto bytes = session::blind_version_sign(util::ustring_from_bytes(env, ed25519_secret_key), session::Platform::android, timestamp);
return util::bytes_from_ustring(env, bytes);
});
}

View File

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

View File

@ -1,18 +1,22 @@
package org.session.libsession.messaging.file_server package org.session.libsession.messaging.file_server
import android.util.Base64
import network.loki.messenger.libsession_util.util.BlindKeyAPI
import nl.komponents.kovenant.Promise import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map import nl.komponents.kovenant.functional.map
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.utilities.await
import org.session.libsignal.utilities.HTTP import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.JsonUtil import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import kotlin.time.Duration.Companion.milliseconds
object FileServerApi { object FileServerApi {
@ -23,6 +27,7 @@ object FileServerApi {
sealed class Error(message: String) : Exception(message) { sealed class Error(message: String) : Exception(message) {
object ParsingFailed : Error("Invalid response.") object ParsingFailed : Error("Invalid response.")
object InvalidURL : Error("Invalid URL.") object InvalidURL : Error("Invalid URL.")
object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
} }
data class Request( data class Request(
@ -105,4 +110,50 @@ object FileServerApi {
val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file") val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file")
return send(request) return send(request)
} }
/**
* Returns the current version of session
* This is effectively proxying (and caching) the response from the github release
* page.
*
* Note that the value is cached and can be up to 30 minutes out of date normally, and up to 24
* hours out of date if we cannot reach the Github API for some reason.
*
* https://github.com/oxen-io/session-file-server/blob/dev/doc/api.yaml#L119
*/
suspend fun getClientVersion(): VersionData {
// Generate the auth signature
val secretKey = MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes
?: throw (Error.NoEd25519KeyPair)
val blindedKeys = BlindKeyAPI.blindVersionKeyPair(secretKey)
val timestamp = 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
val blindedPkHex = "07" + blindedKeys.pubKey.toHexString()
val request = Request(
verb = HTTP.Verb.GET,
endpoint = "session_version",
queryParameters = mapOf("platform" to "android"),
headers = mapOf(
"X-FS-Pubkey" to blindedPkHex,
"X-FS-Timestamp" to timestamp.toString(),
"X-FS-Signature" to Base64.encodeToString(signature, Base64.NO_WRAP)
)
)
// transform the promise into a coroutine
val result = send(request).await()
// map out the result
return JsonUtil.fromJson(result, Map::class.java).let {
VersionData(
statusCode = it["status_code"] as? Int ?: 0,
version = it["result"] as? String ?: "",
updated = it["updated"] as? Double ?: 0.0
)
}
}
} }

View File

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

View File

@ -63,7 +63,6 @@ data class ConfigurationSyncJob(val destination: Destination): Job {
// return a list of batch request objects // return a list of batch request objects
val snodeMessage = MessageSender.buildConfigMessageToSnode(destination.destinationPublicKey(), message) val snodeMessage = MessageSender.buildConfigMessageToSnode(destination.destinationPublicKey(), message)
val authenticated = SnodeAPI.buildAuthenticatedStoreBatchInfo( val authenticated = SnodeAPI.buildAuthenticatedStoreBatchInfo(
destination.destinationPublicKey(),
config.configNamespace(), config.configNamespace(),
snodeMessage snodeMessage
) ?: return@map null // this entry will be null otherwise ) ?: return@map null // this entry will be null otherwise

View File

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

View File

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

View File

@ -19,8 +19,6 @@ import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.messaging.jobs.BatchMessageReceiveJob import org.session.libsession.messaging.jobs.BatchMessageReceiveJob
import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.JobQueue
import org.session.libsession.messaging.jobs.MessageReceiveParameters 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.RawResponse
import org.session.libsession.snode.SnodeAPI import org.session.libsession.snode.SnodeAPI
import org.session.libsession.snode.SnodeModule 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.Log
import org.session.libsignal.utilities.Namespace import org.session.libsignal.utilities.Namespace
import org.session.libsignal.utilities.Snode 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.Timer
import java.util.TimerTask import java.util.TimerTask
import kotlin.time.Duration.Companion.days 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 swarm = SnodeModule.shared.storage.getSwarm(userPublicKey) ?: setOf()
val unusedSnodes = swarm.subtract(usedSnodes) val unusedSnodes = swarm.subtract(usedSnodes)
if (unusedSnodes.isNotEmpty()) { if (unusedSnodes.isNotEmpty()) {
val index = SecureRandom().nextInt(unusedSnodes.size) val index = SECURE_RANDOM.nextInt(unusedSnodes.size)
val nextSnode = unusedSnodes.elementAt(index) val nextSnode = unusedSnodes.elementAt(index)
usedSnodes.add(nextSnode) usedSnodes.add(nextSnode)
Log.d(TAG, "Polling $nextSnode.") Log.d(TAG, "Polling $nextSnode.")
@ -142,8 +140,7 @@ class Poller(private val configFactory: ConfigFactoryProtocol, debounceTimer: Ti
val messages = rawMessages["messages"] as? List<*> val messages = rawMessages["messages"] as? List<*>
val processed = if (!messages.isNullOrEmpty()) { val processed = if (!messages.isNullOrEmpty()) {
SnodeAPI.updateLastMessageHashValueIfPossible(snode, userPublicKey, messages, namespace) SnodeAPI.updateLastMessageHashValueIfPossible(snode, userPublicKey, messages, namespace)
SnodeAPI.removeDuplicates(userPublicKey, messages, namespace, true).mapNotNull { messageBody -> SnodeAPI.removeDuplicates(userPublicKey, messages, namespace, true).mapNotNull { rawMessageAsJSON ->
val rawMessageAsJSON = messageBody as? Map<*, *> ?: return@mapNotNull null
val hashValue = rawMessageAsJSON["hash"] as? String ?: return@mapNotNull null val hashValue = rawMessageAsJSON["hash"] as? String ?: return@mapNotNull null
val b64EncodedBody = rawMessageAsJSON["data"] as? String ?: return@mapNotNull null val b64EncodedBody = rawMessageAsJSON["data"] as? String ?: return@mapNotNull null
val timestamp = rawMessageAsJSON["t"] as? Long ?: SnodeAPI.nowWithOffset val timestamp = rawMessageAsJSON["t"] as? Long ?: SnodeAPI.nowWithOffset

View File

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

View File

@ -10,11 +10,10 @@ import okhttp3.Request
import org.session.libsession.messaging.file_server.FileServerApi import org.session.libsession.messaging.file_server.FileServerApi
import org.session.libsession.utilities.AESGCM import org.session.libsession.utilities.AESGCM
import org.session.libsession.utilities.AESGCM.EncryptionResult import org.session.libsession.utilities.AESGCM.EncryptionResult
import org.session.libsession.utilities.Util
import org.session.libsession.utilities.getBodyForOnionRequest import org.session.libsession.utilities.getBodyForOnionRequest
import org.session.libsession.utilities.getHeadersForOnionRequest import org.session.libsession.utilities.getHeadersForOnionRequest
import org.session.libsignal.crypto.getRandomElement import org.session.libsignal.crypto.secureRandom
import org.session.libsignal.crypto.getRandomElementOrNull import org.session.libsignal.crypto.secureRandomOrNull
import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.database.LokiAPIDatabaseProtocol
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Broadcaster import org.session.libsignal.utilities.Broadcaster
@ -149,7 +148,7 @@ object OnionRequestAPI {
val reusableGuardSnodeCount = reusableGuardSnodes.count() val reusableGuardSnodeCount = reusableGuardSnodes.count()
if (unusedSnodes.count() < (targetGuardSnodeCount - reusableGuardSnodeCount)) { throw InsufficientSnodesException() } if (unusedSnodes.count() < (targetGuardSnodeCount - reusableGuardSnodeCount)) { throw InsufficientSnodesException() }
fun getGuardSnode(): Promise<Snode, Exception> { fun getGuardSnode(): Promise<Snode, Exception> {
val candidate = unusedSnodes.getRandomElementOrNull() val candidate = unusedSnodes.secureRandomOrNull()
?: return Promise.ofFail(InsufficientSnodesException()) ?: return Promise.ofFail(InsufficientSnodesException())
unusedSnodes = unusedSnodes.minus(candidate) unusedSnodes = unusedSnodes.minus(candidate)
Log.d("Loki", "Testing guard snode: $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 // Don't test path snodes as this would reveal the user's IP to them
guardSnodes.minus(reusableGuardSnodes).map { guardSnode -> guardSnodes.minus(reusableGuardSnodes).map { guardSnode ->
val result = listOf( guardSnode ) + (0 until (pathSize - 1)).mapIndexed() { index, _ -> 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 // remove the snode from the unused list and return it
unusedSnodes = unusedSnodes.minus(pathSnode) unusedSnodes = unusedSnodes.minus(pathSnode)
@ -228,9 +227,9 @@ object OnionRequestAPI {
OnionRequestAPI.guardSnodes = guardSnodes OnionRequestAPI.guardSnodes = guardSnodes
fun getPath(paths: List<Path>): Path { fun getPath(paths: List<Path>): Path {
return if (snodeToExclude != null) { return if (snodeToExclude != null) {
paths.filter { !it.contains(snodeToExclude) }.getRandomElement() paths.filter { !it.contains(snodeToExclude) }.secureRandom()
} else { } else {
paths.getRandomElement() paths.secureRandom()
} }
} }
when { when {
@ -273,7 +272,7 @@ object OnionRequestAPI {
path.removeAt(snodeIndex) path.removeAt(snodeIndex)
val unusedSnodes = SnodeAPI.snodePool.minus(oldPaths.flatten()) val unusedSnodes = SnodeAPI.snodePool.minus(oldPaths.flatten())
if (unusedSnodes.isEmpty()) { throw InsufficientSnodesException() } 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 // Don't test the new snode as this would reveal the user's IP
oldPaths.removeAt(pathIndex) oldPaths.removeAt(pathIndex)
val newPaths = oldPaths + listOf( path ) val newPaths = oldPaths + listOf( path )

View File

@ -0,0 +1,13 @@
package org.session.libsession.snode.utilities
import nl.komponents.kovenant.Promise
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
suspend fun <T, E: Throwable> Promise<T, E>.await(): T {
return suspendCoroutine { cont ->
success(cont::resume)
fail(cont::resumeWithException)
}
}

View File

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

View File

@ -13,6 +13,7 @@ import android.text.TextUtils
import android.text.style.StyleSpan import android.text.style.StyleSpan
import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Base64 import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import java.io.* import java.io.*
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.SecureRandom import java.security.SecureRandom
@ -292,15 +293,10 @@ object Util {
@JvmStatic @JvmStatic
fun getSecretBytes(size: Int): ByteArray { fun getSecretBytes(size: Int): ByteArray {
val secret = ByteArray(size) val secret = ByteArray(size)
getSecureRandom().nextBytes(secret) SECURE_RANDOM.nextBytes(secret)
return secret return secret
} }
@JvmStatic
fun getSecureRandom(): SecureRandom {
return SecureRandom()
}
@JvmStatic @JvmStatic
fun getFirstNonEmpty(vararg values: String?): String? { fun getFirstNonEmpty(vararg values: String?): String? {
for (value in values) { for (value in values) {
@ -317,18 +313,14 @@ object Util {
} }
@JvmStatic @JvmStatic
fun <T> getRandomElement(elements: Array<T>): T { fun <T> getRandomElement(elements: Array<T>): T = elements[SECURE_RANDOM.nextInt(elements.size)]
return elements[SecureRandom().nextInt(elements.size)]
}
@JvmStatic @JvmStatic
fun getBoldedString(value: String?): CharSequence { fun getBoldedString(value: String?): CharSequence {
if (value.isNullOrEmpty()) { return "" } if (value.isNullOrEmpty()) { return "" }
val spanned = SpannableString(value) return SpannableString(value).also {
spanned.setSpan(StyleSpan(Typeface.BOLD), 0, it.setSpan(StyleSpan(Typeface.BOLD), 0, it.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spanned.length, }
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spanned
} }
@JvmStatic @JvmStatic
@ -366,34 +358,6 @@ object Util {
val digitGroups = (Math.log10(sizeBytes.toDouble()) / Math.log10(1024.0)).toInt() val digitGroups = (Math.log10(sizeBytes.toDouble()) / Math.log10(1024.0)).toInt()
return DecimalFormat("#,##0.#").format(sizeBytes / Math.pow(1024.0, digitGroups.toDouble())) + " " + units[digitGroups] return DecimalFormat("#,##0.#").format(sizeBytes / Math.pow(1024.0, digitGroups.toDouble())) + " " + units[digitGroups]
} }
/**
* Compares two version strings (for example "1.8.0")
*
* @param version1 the first version string to compare.
* @param version2 the second version string to compare.
* @return an integer indicating the result of the comparison:
* - 0 if the versions are equal
* - a positive number if version1 is greater than version2
* - a negative number if version1 is less than version2
*/
@JvmStatic
fun compareVersions(version1: String, version2: String): Int {
val parts1 = version1.split(".").map { it.toIntOrNull() ?: 0 }
val parts2 = version2.split(".").map { it.toIntOrNull() ?: 0 }
val maxLength = maxOf(parts1.size, parts2.size)
val paddedParts1 = parts1 + List(maxLength - parts1.size) { 0 }
val paddedParts2 = parts2 + List(maxLength - parts2.size) { 0 }
for (i in 0 until maxLength) {
val compare = paddedParts1[i].compareTo(paddedParts2[i])
if (compare != 0) {
return compare
}
}
return 0
}
} }
fun <T, R> T.runIf(condition: Boolean, block: T.() -> R): R where T: R = if (condition) block() else this fun <T, R> T.runIf(condition: Boolean, block: T.() -> R): R where T: R = if (condition) block() else this
@ -430,6 +394,12 @@ fun <E, K: Any, V: Any> Iterable<E>.associateByNotNull(
for(e in this) { it[keySelector(e) ?: continue] = valueTransform(e) ?: continue } for(e in this) { it[keySelector(e) ?: continue] = valueTransform(e) ?: continue }
} }
fun <K: Any, V: Any, W : Any> Map<K, V>.mapValuesNotNull(
valueTransform: (Map.Entry<K, V>) -> W?
): Map<K, W> = mutableMapOf<K, W>().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 * 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 * applied to each element and returns a map where each group key is associated with a list of
@ -440,3 +410,23 @@ fun <E, K: Any, V: Any> Iterable<E>.associateByNotNull(
inline fun <E, K> Iterable<E>.groupByNotNull(keySelector: (E) -> K?): Map<K, List<E>> = LinkedHashMap<K, MutableList<E>>().also { inline fun <E, K> Iterable<E>.groupByNotNull(keySelector: (E) -> K?): Map<K, List<E>> = LinkedHashMap<K, MutableList<E>>().also {
forEach { e -> keySelector(e)?.let { k -> it.getOrPut(k) { mutableListOf() } += e } } forEach { e -> 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 <K, V> buildMutableMap(action: MutableMap<K, V>.() -> Unit): MutableMap<K, V> =
mutableMapOf<K, V>().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 <K : Any, V : Any> Iterable<Pair<K, V?>>.toMapNotNull(): Map<K, V> =
associateByNotNull(Pair<K, V?>::first, Pair<K, V?>::second)
fun Sequence<String>.toByteArray(): ByteArray = ByteArrayOutputStream().use { output ->
forEach { it.byteInputStream().use { input -> input.copyTo(output) } }
output.toByteArray()
}

View File

@ -1,19 +1,23 @@
package org.session.libsignal.crypto 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. * Uses `SecureRandom` to pick an element from this collection.
*/ */
fun <T> Collection<T>.getRandomElementOrNull(): T? { fun <T> Collection<T>.secureRandomOrNull(): T? {
if (isEmpty()) return null 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) return elementAtOrNull(index)
} }
/** /**
* Uses `SecureRandom` to pick an element from this collection. * Uses `SecureRandom` to pick an element from this collection.
*
* @throws [NullPointerException] if the [Collection] is empty
*/ */
fun <T> Collection<T>.getRandomElement(): T { fun <T> Collection<T>.secureRandom(): T {
return getRandomElementOrNull()!! return secureRandomOrNull()!!
} }
fun <T> Collection<T>.shuffledRandom(): List<T> = shuffled(SECURE_RANDOM)

View File

@ -1,13 +1,13 @@
package org.session.libsignal.streams; package org.session.libsignal.streams;
import static org.session.libsignal.crypto.CipherUtil.CIPHER_LOCK; 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.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException; import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
@ -80,7 +80,7 @@ public class ProfileCipherOutputStream extends DigestingOutputStream {
private byte[] generateNonce() { private byte[] generateNonce() {
byte[] nonce = new byte[12]; byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce); SECURE_RANDOM.nextBytes(nonce);
return nonce; return nonce;
} }

View File

@ -1,5 +1,7 @@
package org.session.libsignal.utilities; package org.session.libsignal.utilities;
import androidx.annotation.NonNull;
/** /**
* <p>Encodes and decodes to and from Base64 notation.</p> * <p>Encodes and decodes to and from Base64 notation.</p>
* <p>Homepage: <a href="http://iharder.net/base64">http://iharder.net/base64</a>.</p> * <p>Homepage: <a href="http://iharder.net/base64">http://iharder.net/base64</a>.</p>
@ -714,7 +716,7 @@ public class Base64
* @throws NullPointerException if source array is null * @throws NullPointerException if source array is null
* @since 1.4 * @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, // 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're not going to have an java.io.IOException thrown, so
// we should not force the user to have to catch it. // we should not force the user to have to catch it.

View File

@ -5,8 +5,7 @@ import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.Response import org.session.libsignal.utilities.Util.SECURE_RANDOM
import java.security.SecureRandom
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
@ -35,7 +34,7 @@ object HTTP {
override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() } override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() }
} }
val sslContext = SSLContext.getInstance("SSL") val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf( trustManager ), SecureRandom()) sslContext.init(null, arrayOf( trustManager ), SECURE_RANDOM)
OkHttpClient().newBuilder() OkHttpClient().newBuilder()
.sslSocketFactory(sslContext.socketFactory, trustManager) .sslSocketFactory(sslContext.socketFactory, trustManager)
.hostnameVerifier { _, _ -> true } .hostnameVerifier { _, _ -> true }
@ -55,7 +54,7 @@ object HTTP {
override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() } override fun getAcceptedIssuers(): Array<X509Certificate> { return arrayOf() }
} }
val sslContext = SSLContext.getInstance("SSL") val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, arrayOf( trustManager ), SecureRandom()) sslContext.init(null, arrayOf( trustManager ), SECURE_RANDOM)
return OkHttpClient().newBuilder() return OkHttpClient().newBuilder()
.sslSocketFactory(sslContext.socketFactory, trustManager) .sslSocketFactory(sslContext.socketFactory, trustManager)
.hostnameVerifier { _, _ -> true } .hostnameVerifier { _, _ -> true }

View File

@ -1,9 +1,25 @@
package org.session.libsignal.utilities package org.session.libsignal.utilities
class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val version: String) { import android.annotation.SuppressLint
import android.util.LruCache
/**
* Create a Snode from a "-" delimited String if valid, null otherwise.
*/
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://") 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"), GetSwarm("get_snodes_for_pubkey"),
Retrieve("retrieve"), Retrieve("retrieve"),
SendMessage("store"), SendMessage("store"),
@ -19,17 +35,37 @@ class Snode(val address: String, val port: Int, val publicKeySet: KeySet?, val v
data class KeySet(val ed25519Key: String, val x25519Key: String) data class KeySet(val ed25519Key: String, val x25519Key: String)
override fun equals(other: Any?): Boolean { override fun equals(other: Any?) = other is Snode && address == other.address && port == other.port
return if (other is Snode) { override fun hashCode(): Int = address.hashCode() xor port.hashCode()
address == other.address && port == other.port override fun toString(): String = "$address:$port"
} else {
false companion object {
private val CACHE = LruCache<String, Version>(100)
@SuppressLint("NotConstructor")
@Synchronized
fun Version(value: String) = CACHE[value] ?: Snode.Version(value).also { CACHE.put(value, it) }
fun Version(parts: List<Int>) = Version(parts.joinToString("."))
}
@JvmInline
value class Version(val value: ULong) {
companion object {
val ZERO = Version(0UL)
private const val MASK_BITS = 16
private const val MASK = 0xFFFFUL
} }
}
override fun hashCode(): Int { internal constructor(value: String): this(
return address.hashCode() xor port.hashCode() value.splitToSequence(".")
} .take(4)
.map { it.toULongOrNull() ?: 0UL }
.foldIndexed(0UL) { i, acc, it ->
it.coerceAtMost(MASK) shl (3 - i) * MASK_BITS or acc
}
)
override fun toString(): String { return "$address:$port" } operator fun compareTo(other: Version): Int = value.compareTo(other.value)
}
} }

View File

@ -12,12 +12,10 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class Util { public class Util {
public static SecureRandom SECURE_RANDOM = new SecureRandom();
public static byte[] join(byte[]... input) { public static byte[] join(byte[]... input) {
try { try {
@ -67,7 +65,7 @@ public class Util {
} }
public static boolean isEmpty(String value) { 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) { 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 { public static String readFully(InputStream in) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream(); ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
@ -98,7 +89,7 @@ public class Util {
in.close(); in.close();
return new String(bout.toByteArray()); return bout.toString();
} }
public static void readFully(InputStream in, byte[] buffer) throws IOException { public static void readFully(InputStream in, byte[] buffer) throws IOException {
@ -146,9 +137,4 @@ public class Util {
} }
return (int)value; return (int)value;
} }
public static <T> List<T> immutableList(T... elements) {
return Collections.unmodifiableList(Arrays.asList(elements.clone()));
}
} }