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

View File

@ -7,8 +7,6 @@ import com.squareup.phrase.Phrase
import network.loki.messenger.R
import org.session.libsession.LocalisedTimeUtil
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 kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

View File

@ -27,7 +27,11 @@ import org.thoughtcrime.securesms.groups.JoinCommunityFragment
@AndroidEntryPoint
class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate {
private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * 0.94).toInt() }
companion object{
const val PEEK_RATIO = 0.94f
}
private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * PEEK_RATIO).toInt() }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,

View File

@ -1,10 +1,13 @@
package org.thoughtcrime.securesms.conversation.start.newmessage
import androidx.compose.animation.AnimatedVisibility
import android.graphics.Rect
import android.os.Build
import android.view.ViewTreeObserver
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -15,23 +18,31 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import network.loki.messenger.R
import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton
import org.thoughtcrime.securesms.conversation.start.StartConversationFragment.Companion.PEEK_RATIO
import org.thoughtcrime.securesms.ui.LoadingArcOr
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.components.AppBar
import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
@ -39,7 +50,13 @@ import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
import org.thoughtcrime.securesms.ui.components.SessionTabRow
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors
import kotlin.math.max
private val TITLES = listOf(R.string.accountIdEnter, R.string.qrScan)
@ -76,11 +93,48 @@ private fun EnterAccountId(
callbacks: Callbacks,
onHelp: () -> Unit = {}
) {
Column(
modifier = Modifier
// the scaffold is required to provide the contentPadding. That contentPadding is needed
// to properly handle the ime padding.
Scaffold() { contentPadding ->
// we need this extra surface to handle nested scrolling properly,
// because this scrollable component is inside a bottomSheet dialog which is itself scrollable
Surface(
modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()),
color = LocalColors.current.backgroundSecondary
) {
var accountModifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
//<<<<<<< HEAD
// BorderlessButtonWithIcon(
// text = stringResource(R.string.messageNewDescriptionMobile),
// modifier = Modifier
// .contentDescription(R.string.AccessibilityId_help_desk_link)
// .padding(horizontal = LocalDimensions.current.mediumSpacing)
// .fillMaxWidth(),
// style = LocalType.current.small,
// color = LocalColors.current.textSecondary,
// iconRes = R.drawable.ic_circle_question_mark,
// onClick = onHelp
// )
// }
//=======
// There is a known issue with the ime padding on android versions below 30
/// So 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()
}
Column(
modifier = accountModifier
) {
Column(
modifier = Modifier.padding(vertical = LocalDimensions.current.spacing),
@ -132,6 +186,33 @@ private fun EnterAccountId(
}
}
}
}
}
@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
@Composable

View File

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

View File

@ -178,7 +178,6 @@ import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
import org.thoughtcrime.securesms.recoverypassword.RecoveryPasswordActivity
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.util.ActivityDispatcher
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@ -190,7 +189,6 @@ import org.thoughtcrime.securesms.util.isScrolledToBottom
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.start
import org.thoughtcrime.securesms.util.toPx
private const val TAG = "ConversationActivityV2"
@ -1651,8 +1649,15 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val text = getMessageBody()
val userPublicKey = textSecurePreferences.getLocalNumber()
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
start<RecoveryPasswordActivity>()
if (seed in text && !isNoteToSelf && !hasPermissionToSendSeed) {
showSessionDialog {
title(R.string.warning)
text(R.string.recoveryPasswordWarningSendDescription)
button(R.string.send) { sendTextOnlyMessage(true) }
cancelButton()
}
return null
}
// Create the message
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.IOException
import java.io.UnsupportedEncodingException
import java.security.SecureRandom
import java.util.Collections
import java.util.concurrent.TimeUnit
import kotlin.math.max
@ -247,32 +246,6 @@ object Util {
return result
}
fun getSecretBytes(size: Int): ByteArray {
return getSecretBytes(SecureRandom(), size)
}
fun getSecretBytes(secureRandom: SecureRandom, size: Int): ByteArray {
val secret = ByteArray(size)
secureRandom.nextBytes(secret)
return secret
}
fun <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? {
return if (uri == null) null
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;
import static org.session.libsignal.utilities.Util.SECURE_RANDOM;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import org.session.libsession.utilities.TextSecurePreferences;
import java.security.SecureRandom;
/**
* A provider that is responsible for creating or retrieving the AttachmentSecret model.
*
@ -81,9 +81,8 @@ public class AttachmentSecretProvider {
}
private AttachmentSecret createAndStoreAttachmentSecret(@NonNull Context context) {
SecureRandom random = new SecureRandom();
byte[] secret = new byte[32];
random.nextBytes(secret);
SECURE_RANDOM.nextBytes(secret);
AttachmentSecret attachmentSecret = new AttachmentSecret(null, null, secret);
storeAttachmentSecret(context, attachmentSecret);

View File

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

View File

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

View File

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

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 EMPTY_VERSION = "0.0.0"
// endregion
}
@ -175,15 +173,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val database = databaseHelper.readableDatabase
return database.get(snodePoolTable, "${Companion.dummyKey} = ?", wrap("dummy_key")) { cursor ->
val snodePoolAsString = cursor.getString(cursor.getColumnIndexOrThrow(snodePool))
snodePoolAsString.split(", ").mapNotNull { snodeAsString ->
val components = snodeAsString.split("-")
val address = components[0]
val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null
val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null
val x25519Key = components.getOrNull(3) ?: return@mapNotNull null
val version = components.getOrNull(4) ?: EMPTY_VERSION
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version)
}
snodePoolAsString.split(", ").mapNotNull(::Snode)
}?.toSet() ?: setOf()
}
@ -231,18 +221,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val database = databaseHelper.readableDatabase
fun get(indexPath: String): Snode? {
return database.get(onionRequestPathTable, "${Companion.indexPath} = ?", wrap(indexPath)) { cursor ->
val snodeAsString = cursor.getString(cursor.getColumnIndexOrThrow(snode))
val components = snodeAsString.split("-")
val address = components[0]
val port = components.getOrNull(1)?.toIntOrNull()
val ed25519Key = components.getOrNull(2)
val x25519Key = components.getOrNull(3)
val version = components.getOrNull(4) ?: EMPTY_VERSION
if (port != null && ed25519Key != null && x25519Key != null) {
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version)
} else {
null
}
Snode(cursor.getString(cursor.getColumnIndexOrThrow(snode)))
}
}
val result = mutableListOf<List<Snode>>()
@ -276,15 +255,7 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(
val database = databaseHelper.readableDatabase
return database.get(swarmTable, "${Companion.swarmPublicKey} = ?", wrap(publicKey)) { cursor ->
val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm))
swarmAsString.split(", ").mapNotNull { targetAsString ->
val components = targetAsString.split("-")
val address = components[0]
val port = components.getOrNull(1)?.toIntOrNull() ?: return@mapNotNull null
val ed25519Key = components.getOrNull(2) ?: return@mapNotNull null
val x25519Key = components.getOrNull(3) ?: return@mapNotNull null
val version = components.getOrNull(4) ?: EMPTY_VERSION
Snode(address, port, Snode.KeySet(ed25519Key, x25519Key), version)
}
swarmAsString.split(", ").mapNotNull(::Snode)
}?.toSet()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -20,7 +20,12 @@ import org.session.libsession.utilities.AppTextSecurePreferences
val LocalColors = compositionLocalOf <ThemeColors> { ClassicDark() }
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.
@ -29,15 +34,15 @@ var selectedTheme: ThemeColors? = null
fun SessionMaterialTheme(
content: @Composable () -> Unit
) {
// set the theme data if it hasn't been done yet
if(selectedTheme == null) {
// Some values can be set from the preferences, and if not should fallback to a default value
val context = LocalContext.current
val preferences = AppTextSecurePreferences(context)
selectedTheme = preferences.getComposeTheme()
}
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,
LocalContentColor provides colors.text,
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
import android.content.Context
import org.session.libsignal.crypto.shuffledRandom
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.SettableFuture
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import org.thoughtcrime.securesms.webrtc.video.Camera
import org.thoughtcrime.securesms.webrtc.video.CameraEventListener
import org.thoughtcrime.securesms.webrtc.video.CameraState
@ -22,9 +24,7 @@ import org.webrtc.SurfaceTextureHelper
import org.webrtc.VideoSink
import org.webrtc.VideoSource
import org.webrtc.VideoTrack
import java.security.SecureRandom
import java.util.concurrent.ExecutionException
import kotlin.random.asKotlinRandom
class PeerConnectionWrapper(private val context: Context,
private val factory: PeerConnectionFactory,
@ -49,8 +49,7 @@ class PeerConnectionWrapper(private val context: Context,
private var isInitiator = false
private fun initPeerConnection() {
val random = SecureRandom().asKotlinRandom()
val iceServers = listOf("freyr","angus","hereford","holstein", "brahman").shuffled(random).take(2).map { sub ->
val iceServers = listOf("freyr","angus","hereford","holstein", "brahman").shuffledRandom().take(2).map { sub ->
PeerConnection.IceServer.builder("turn:$sub.getsession.org")
.setUsername("session202111")
.setPassword("053c268164bc7bd7")

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
contacts.cpp
conversation.cpp
blinded_key.cpp
util.cpp)
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
import android.util.Base64
import network.loki.messenger.libsession_util.util.BlindKeyAPI
import nl.komponents.kovenant.Promise
import nl.komponents.kovenant.functional.map
import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.utilities.await
import org.session.libsignal.utilities.HTTP
import org.session.libsignal.utilities.JsonUtil
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.toHexString
import kotlin.time.Duration.Companion.milliseconds
object FileServerApi {
@ -23,6 +27,7 @@ object FileServerApi {
sealed class Error(message: String) : Exception(message) {
object ParsingFailed : Error("Invalid response.")
object InvalidURL : Error("Invalid URL.")
object NoEd25519KeyPair : Error("Couldn't find ed25519 key pair.")
}
data class Request(
@ -105,4 +110,50 @@ object FileServerApi {
val request = Request(verb = HTTP.Verb.GET, endpoint = "file/$file")
return send(request)
}
/**
* Returns the current version of session
* This is effectively proxying (and caching) the response from the github release
* page.
*
* Note that the value is cached and can be up to 30 minutes out of date normally, and up to 24
* hours out of date if we cannot reach the Github API for some reason.
*
* https://github.com/oxen-io/session-file-server/blob/dev/doc/api.yaml#L119
*/
suspend fun getClientVersion(): VersionData {
// Generate the auth signature
val secretKey = MessagingModuleConfiguration.shared.getUserED25519KeyPair()?.secretKey?.asBytes
?: throw (Error.NoEd25519KeyPair)
val blindedKeys = BlindKeyAPI.blindVersionKeyPair(secretKey)
val timestamp = 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
val snodeMessage = MessageSender.buildConfigMessageToSnode(destination.destinationPublicKey(), message)
val authenticated = SnodeAPI.buildAuthenticatedStoreBatchInfo(
destination.destinationPublicKey(),
config.configNamespace(),
snodeMessage
) ?: return@map null // this entry will be null otherwise

View File

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

View File

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

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

View File

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

View File

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

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

View File

@ -13,6 +13,7 @@ import android.text.TextUtils
import android.text.style.StyleSpan
import org.session.libsignal.utilities.Log
import org.session.libsignal.utilities.Base64
import org.session.libsignal.utilities.Util.SECURE_RANDOM
import java.io.*
import java.nio.charset.StandardCharsets
import java.security.SecureRandom
@ -292,15 +293,10 @@ object Util {
@JvmStatic
fun getSecretBytes(size: Int): ByteArray {
val secret = ByteArray(size)
getSecureRandom().nextBytes(secret)
SECURE_RANDOM.nextBytes(secret)
return secret
}
@JvmStatic
fun getSecureRandom(): SecureRandom {
return SecureRandom()
}
@JvmStatic
fun getFirstNonEmpty(vararg values: String?): String? {
for (value in values) {
@ -317,18 +313,14 @@ object Util {
}
@JvmStatic
fun <T> getRandomElement(elements: Array<T>): T {
return elements[SecureRandom().nextInt(elements.size)]
}
fun <T> getRandomElement(elements: Array<T>): T = elements[SECURE_RANDOM.nextInt(elements.size)]
@JvmStatic
fun getBoldedString(value: String?): CharSequence {
if (value.isNullOrEmpty()) { return "" }
val spanned = SpannableString(value)
spanned.setSpan(StyleSpan(Typeface.BOLD), 0,
spanned.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spanned
return SpannableString(value).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, it.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
@JvmStatic
@ -366,34 +358,6 @@ object Util {
val digitGroups = (Math.log10(sizeBytes.toDouble()) / Math.log10(1024.0)).toInt()
return DecimalFormat("#,##0.#").format(sizeBytes / Math.pow(1024.0, digitGroups.toDouble())) + " " + units[digitGroups]
}
/**
* Compares two version strings (for example "1.8.0")
*
* @param version1 the first version string to compare.
* @param version2 the second version string to compare.
* @return an integer indicating the result of the comparison:
* - 0 if the versions are equal
* - a positive number if version1 is greater than version2
* - a negative number if version1 is less than version2
*/
@JvmStatic
fun compareVersions(version1: String, version2: String): Int {
val parts1 = version1.split(".").map { it.toIntOrNull() ?: 0 }
val parts2 = version2.split(".").map { it.toIntOrNull() ?: 0 }
val maxLength = maxOf(parts1.size, parts2.size)
val paddedParts1 = parts1 + List(maxLength - parts1.size) { 0 }
val paddedParts2 = parts2 + List(maxLength - parts2.size) { 0 }
for (i in 0 until maxLength) {
val compare = paddedParts1[i].compareTo(paddedParts2[i])
if (compare != 0) {
return compare
}
}
return 0
}
}
fun <T, 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 }
}
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
* 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 {
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
import java.security.SecureRandom
import org.session.libsignal.utilities.Util.SECURE_RANDOM
/**
* 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
val index = SecureRandom().nextInt(size) // SecureRandom() should be cryptographically secure
val index = SECURE_RANDOM.nextInt(size) // SecureRandom should be cryptographically secure
return elementAtOrNull(index)
}
/**
* Uses `SecureRandom` to pick an element from this collection.
*
* @throws [NullPointerException] if the [Collection] is empty
*/
fun <T> Collection<T>.getRandomElement(): T {
return getRandomElementOrNull()!!
fun <T> Collection<T>.secureRandom(): T {
return secureRandomOrNull()!!
}
fun <T> Collection<T>.shuffledRandom(): List<T> = shuffled(SECURE_RANDOM)

View File

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

View File

@ -1,5 +1,7 @@
package org.session.libsignal.utilities;
import androidx.annotation.NonNull;
/**
* <p>Encodes and decodes to and from Base64 notation.</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
* @since 1.4
*/
public static String encodeBytes( byte[] source ) {
public static String encodeBytes(@NonNull byte[] source ) {
// Since we're not going to have the GZIP encoding turned on,
// we're not going to have an java.io.IOException thrown, so
// we should not force the user to have to catch it.

View File

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

View File

@ -1,9 +1,25 @@
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://")
public enum class Method(val rawValue: String) {
enum class Method(val rawValue: String) {
GetSwarm("get_snodes_for_pubkey"),
Retrieve("retrieve"),
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)
override fun equals(other: Any?): Boolean {
return if (other is Snode) {
address == other.address && port == other.port
} else {
false
}
override fun equals(other: Any?) = other is Snode && address == other.address && port == other.port
override fun hashCode(): Int = address.hashCode() xor port.hashCode()
override fun toString(): String = "$address:$port"
companion object {
private val CACHE = 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("."))
}
override fun hashCode(): Int {
return address.hashCode() xor port.hashCode()
@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 toString(): String { return "$address:$port" }
internal constructor(value: String): this(
value.splitToSequence(".")
.take(4)
.map { it.toULongOrNull() ?: 0UL }
.foldIndexed(0UL) { i, acc, it ->
it.coerceAtMost(MASK) shl (3 - i) * MASK_BITS or acc
}
)
operator fun compareTo(other: Version): Int = value.compareTo(other.value)
}
}

View File

@ -12,12 +12,10 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class Util {
public static SecureRandom SECURE_RANDOM = new SecureRandom();
public static byte[] join(byte[]... input) {
try {
@ -67,7 +65,7 @@ public class Util {
}
public static boolean isEmpty(String value) {
return value == null || value.trim().length() == 0;
return value == null || value.trim().isEmpty();
}
public static byte[] getSecretBytes(int size) {
@ -80,13 +78,6 @@ public class Util {
}
}
public static byte[] getRandomLengthBytes(int maxSize) {
SecureRandom secureRandom = new SecureRandom();
byte[] result = new byte[secureRandom.nextInt(maxSize) + 1];
secureRandom.nextBytes(result);
return result;
}
public static String readFully(InputStream in) throws IOException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
@ -98,7 +89,7 @@ public class Util {
in.close();
return new String(bout.toByteArray());
return bout.toString();
}
public static void readFully(InputStream in, byte[] buffer) throws IOException {
@ -146,9 +137,4 @@ public class Util {
}
return (int)value;
}
public static <T> List<T> immutableList(T... elements) {
return Collections.unmodifiableList(Arrays.asList(elements.clone()));
}
}