Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,51 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
}
)

switchPref(
title = DSLSettingsText.from("Randomize receipt timing"),
summary = DSLSettingsText.from("Add random delay (0.3-5s) to receipts (delivery, read, viewed) to help prevent some timing correlation attacks"),
isChecked = state.receiptDeliveryDelay,
onClick = {
viewModel.setReceiptDeliveryDelayEnabled(!state.receiptDeliveryDelay)
}
)

switchPref(
title = DSLSettingsText.from("Disable delivery receipts for edits"),
summary = DSLSettingsText.from("Don't send delivery receipts when you receive edited messages"),
isChecked = state.deliveryReceiptsForEdits,
onClick = {
viewModel.setDeliveryReceiptsForEditsDisabled(!state.deliveryReceiptsForEdits)
}
)

switchPref(
title = DSLSettingsText.from("Disable delivery receipts for reactions"),
summary = DSLSettingsText.from("Don't send delivery receipts when you receive reactions"),
isChecked = state.deliveryReceiptsForReactions,
onClick = {
viewModel.setDeliveryReceiptsForReactionsDisabled(!state.deliveryReceiptsForReactions)
}
)

switchPref(
title = DSLSettingsText.from("Disable delivery receipts for deletes"),
summary = DSLSettingsText.from("Don't send delivery receipts when you receive remote delete messages"),
isChecked = state.deliveryReceiptsForDeletes,
onClick = {
viewModel.setDeliveryReceiptsForDeletesDisabled(!state.deliveryReceiptsForDeletes)
}
)

switchPref(
title = DSLSettingsText.from("Disable delivery receipts to blocked users"),
summary = DSLSettingsText.from("Don't allow blocked contacts to know when you receive their messages (you should enable this to protect against timing attacks)"),
isChecked = state.deliveryReceiptsForBlocked,
onClick = {
viewModel.setDeliveryReceiptsForBlockedDisabled(!state.deliveryReceiptsForBlocked)
}
)

dividerPref()

sectionHeaderPref(R.string.PrivacySettingsFragment__disappearing_messages)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ data class PrivacySettingsState(
val blockedCount: Int,
val readReceipts: Boolean,
val typingIndicators: Boolean,
val receiptDeliveryDelay: Boolean,
val deliveryReceiptsForEdits: Boolean,
val deliveryReceiptsForReactions: Boolean,
val deliveryReceiptsForDeletes: Boolean,
val deliveryReceiptsForBlocked: Boolean,
val screenLock: Boolean,
val screenLockActivityTimeout: Long,
val screenSecurity: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@ class PrivacySettingsViewModel(
refresh()
}

fun setReceiptDeliveryDelayEnabled(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.RECEIPT_DELIVERY_DELAY_PREF, enabled).apply()
refresh()
}

fun setDeliveryReceiptsForEditsDisabled(disabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.DELIVERY_RECEIPTS_FOR_EDITS_PREF, disabled).apply()
refresh()
}

fun setDeliveryReceiptsForReactionsDisabled(disabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.DELIVERY_RECEIPTS_FOR_REACTIONS_PREF, disabled).apply()
refresh()
}

fun setDeliveryReceiptsForDeletesDisabled(disabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.DELIVERY_RECEIPTS_FOR_DELETES_PREF, disabled).apply()
refresh()
}

fun setDeliveryReceiptsForBlockedDisabled(disabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.DELIVERY_RECEIPTS_FOR_BLOCKED_PREF, disabled).apply()
refresh()
}

fun setScreenSecurityEnabled(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.SCREEN_SECURITY_PREF, enabled).apply()
refresh()
Expand Down Expand Up @@ -71,6 +96,11 @@ class PrivacySettingsViewModel(
blockedCount = 0,
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(AppDependencies.application),
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(AppDependencies.application),
receiptDeliveryDelay = TextSecurePreferences.isReceiptDeliveryDelayEnabled(AppDependencies.application),
deliveryReceiptsForEdits = TextSecurePreferences.isDeliveryReceiptsForEditsDisabled(AppDependencies.application),
deliveryReceiptsForReactions = TextSecurePreferences.isDeliveryReceiptsForReactionsDisabled(AppDependencies.application),
deliveryReceiptsForDeletes = TextSecurePreferences.isDeliveryReceiptsForDeletesDisabled(AppDependencies.application),
deliveryReceiptsForBlocked = TextSecurePreferences.isDeliveryReceiptsForBlockedDisabled(AppDependencies.application),
screenLock = SignalStore.settings.screenLockEnabled,
screenLockActivityTimeout = SignalStore.settings.screenLockTimeout,
screenSecurity = TextSecurePreferences.isScreenSecurityEnabled(AppDependencies.application),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.ContentHint;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
Expand All @@ -28,6 +29,7 @@
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;

import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

Expand All @@ -42,6 +44,11 @@ public class SendDeliveryReceiptJob extends BaseJob {

private static final String TAG = Log.tag(SendReadReceiptJob.class);

// Privacy enhancement: randomized delay to prevent timing attacks
private static final long MIN_DELAY_MS = 300;
private static final long MAX_DELAY_MS = 5000; // 5 seconds
private static final SecureRandom secureRandom = new SecureRandom();

private final RecipientId recipientId;
private final long messageSentTimestamp;
private final long timestamp;
Expand All @@ -55,13 +62,29 @@ public SendDeliveryReceiptJob(@NonNull RecipientId recipientId, long messageSent
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.setQueue(recipientId.toQueueKey())
.setInitialDelay(getRandomDelayIfEnabled())
.build(),
recipientId,
messageSentTimestamp,
messageId,
System.currentTimeMillis());
}

/**
* Generates a random delay between MIN_DELAY_MS and MAX_DELAY_MS to prevent timing attacks,
* but only if the feature is enabled. This makes it difficult for an adversary to correlate
* message receipt with delivery receipt timing.
*
* @return A random delay in milliseconds if enabled, 0 otherwise
*/
private static long getRandomDelayIfEnabled() {
if (!TextSecurePreferences.isReceiptDeliveryDelayEnabled(AppDependencies.getApplication())) {
return 0;
}
long range = MAX_DELAY_MS - MIN_DELAY_MS;
return MIN_DELAY_MS + (long)(secureRandom.nextDouble() * range);
}

private SendDeliveryReceiptJob(@NonNull Job.Parameters parameters,
@NonNull RecipientId recipientId,
long messageSentTimestamp,
Expand Down Expand Up @@ -108,6 +131,11 @@ public void onRun() throws IOException, UntrustedIdentityException, Undeliverabl
return;
}

if (recipient.isBlocked() && TextSecurePreferences.isDeliveryReceiptsForBlockedDisabled(context)) {
Log.w(TAG, "Refusing to send delivery receipt to blocked recipient");
return;
}

if (recipient.isUnregistered()) {
Log.w(TAG, recipient.getId() + " is unregistered!");
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;

import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand All @@ -47,6 +48,11 @@ public class SendReadReceiptJob extends BaseJob {

static final int MAX_TIMESTAMPS = 500;

// Privacy enhancement: randomized delay to prevent timing attacks
private static final long MIN_DELAY_MS = 300;
private static final long MAX_DELAY_MS = 5000; // 5 seconds
private static final SecureRandom secureRandom = new SecureRandom();

private static final String KEY_THREAD = "thread";
private static final String KEY_ADDRESS = "address";
private static final String KEY_RECIPIENT = "recipient";
Expand All @@ -67,6 +73,7 @@ public SendReadReceiptJob(long threadId, @NonNull RecipientId recipientId, List<
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.setQueue(recipientId.toQueueKey())
.setInitialDelay(getRandomDelayIfEnabled())
.build(),
threadId,
recipientId,
Expand All @@ -75,6 +82,21 @@ public SendReadReceiptJob(long threadId, @NonNull RecipientId recipientId, List<
System.currentTimeMillis());
}

/**
* Generates a random delay between MIN_DELAY_MS and MAX_DELAY_MS to prevent timing attacks,
* but only if the feature is enabled. This makes it difficult for an adversary to correlate
* message read events with read receipt timing.
*
* @return A random delay in milliseconds if enabled, 0 otherwise
*/
private static long getRandomDelayIfEnabled() {
if (!TextSecurePreferences.isReceiptDeliveryDelayEnabled(AppDependencies.getApplication())) {
return 0;
}
long range = MAX_DELAY_MS - MIN_DELAY_MS;
return MIN_DELAY_MS + (long)(secureRandom.nextDouble() * range);
}

private SendReadReceiptJob(@NonNull Job.Parameters parameters,
long threadId,
@NonNull RecipientId recipientId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;

import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -49,6 +50,11 @@ public class SendViewedReceiptJob extends BaseJob {

private static final String TAG = Log.tag(SendViewedReceiptJob.class);

// Privacy enhancement: randomized delay to prevent timing attacks
private static final long MIN_DELAY_MS = 300;
private static final long MAX_DELAY_MS = 5000; // 5 seconds
private static final SecureRandom secureRandom = new SecureRandom();

private static final String KEY_THREAD = "thread";
private static final String KEY_ADDRESS = "address";
private static final String KEY_RECIPIENT = "recipient";
Expand All @@ -71,6 +77,7 @@ private SendViewedReceiptJob(long threadId, @NonNull RecipientId recipientId, @N
.addConstraint(NetworkConstraint.KEY)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(Parameters.UNLIMITED)
.setInitialDelay(getRandomDelayIfEnabled())
.build(),
threadId,
recipientId,
Expand All @@ -79,6 +86,21 @@ private SendViewedReceiptJob(long threadId, @NonNull RecipientId recipientId, @N
System.currentTimeMillis());
}

/**
* Generates a random delay between MIN_DELAY_MS and MAX_DELAY_MS to prevent timing attacks,
* but only if the feature is enabled. This makes it difficult for an adversary to correlate
* message view events with viewed receipt timing.
*
* @return A random delay in milliseconds if enabled, 0 otherwise
*/
private static long getRandomDelayIfEnabled() {
if (!TextSecurePreferences.isReceiptDeliveryDelayEnabled(AppDependencies.getApplication())) {
return 0;
}
long range = MAX_DELAY_MS - MIN_DELAY_MS;
return MIN_DELAY_MS + (long)(secureRandom.nextDouble() * range);
}

private SendViewedReceiptJob(@NonNull Parameters parameters,
long threadId,
@NonNull RecipientId recipientId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,16 @@ object DataMessageProcessor {
}

if (metadata.sealedSender && messageId != null) {
SignalExecutors.BOUNDED.execute { AppDependencies.jobManager.add(SendDeliveryReceiptJob(senderRecipient.id, message.timestamp!!, messageId)) }
// Check if we should skip delivery receipts based on message type
val shouldSendReceipt = when {
message.reaction != null -> !TextSecurePreferences.isDeliveryReceiptsForReactionsDisabled(context)
message.hasRemoteDelete -> !TextSecurePreferences.isDeliveryReceiptsForDeletesDisabled(context)
else -> true
}

if (shouldSendReceipt) {
SignalExecutors.BOUNDED.execute { AppDependencies.jobManager.add(SendDeliveryReceiptJob(senderRecipient.id, message.timestamp!!, messageId)) }
}
} else if (!metadata.sealedSender) {
if (RecipientUtil.shouldHaveProfileKey(threadRecipient)) {
Log.w(MessageContentProcessor.TAG, "Received an unsealed sender message from " + senderRecipient.id + ", but they should already have our profile key. Correcting.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.MessageConstraintsUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.hasAudio
import org.thoughtcrime.securesms.util.hasSharedContact
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
Expand Down Expand Up @@ -96,8 +97,10 @@ object EditMessageProcessor {
}

if (insertResult != null) {
SignalExecutors.BOUNDED.execute {
AppDependencies.jobManager.add(SendDeliveryReceiptJob(senderRecipient.id, message.timestamp!!, MessageId(insertResult.messageId)))
if (!TextSecurePreferences.isDeliveryReceiptsForEditsDisabled(context)) {
SignalExecutors.BOUNDED.execute {
AppDependencies.jobManager.add(SendDeliveryReceiptJob(senderRecipient.id, message.timestamp!!, MessageId(insertResult.messageId)))
}
}

if (targetMessage.expireStarted > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ public class TextSecurePreferences {
public static final String DIRECT_CAPTURE_CAMERA_ID = "pref_direct_capture_camera_id";
public static final String ALWAYS_RELAY_CALLS_PREF = "pref_turn_only";
public static final String READ_RECEIPTS_PREF = "pref_read_receipts";
public static final String RECEIPT_DELIVERY_DELAY_PREF = "pref_receipt_delivery_delay";
public static final String DELIVERY_RECEIPTS_FOR_EDITS_PREF = "pref_delivery_receipts_for_edits";
public static final String DELIVERY_RECEIPTS_FOR_REACTIONS_PREF = "pref_delivery_receipts_for_reactions";
public static final String DELIVERY_RECEIPTS_FOR_DELETES_PREF = "pref_delivery_receipts_for_deletes";
public static final String DELIVERY_RECEIPTS_FOR_BLOCKED_PREF = "pref_delivery_receipts_for_blocked";
public static final String INCOGNITO_KEYBOARD_PREF = "pref_incognito_keyboard";
public static final String UNAUTHORIZED_RECEIVED = "pref_unauthorized_received";
private static final String SUCCESSFUL_DIRECTORY_PREF = "pref_successful_directory";
Expand Down Expand Up @@ -417,6 +422,46 @@ public static void setReadReceiptsEnabled(Context context, boolean enabled) {
setBooleanPreference(context, READ_RECEIPTS_PREF, enabled);
}

public static boolean isReceiptDeliveryDelayEnabled(Context context) {
return getBooleanPreference(context, RECEIPT_DELIVERY_DELAY_PREF, true);
}

public static void setReceiptDeliveryDelayEnabled(Context context, boolean enabled) {
setBooleanPreference(context, RECEIPT_DELIVERY_DELAY_PREF, enabled);
}

public static boolean isDeliveryReceiptsForEditsDisabled(Context context) {
return getBooleanPreference(context, DELIVERY_RECEIPTS_FOR_EDITS_PREF, false);
}

public static void setDeliveryReceiptsForEditsDisabled(Context context, boolean disabled) {
setBooleanPreference(context, DELIVERY_RECEIPTS_FOR_EDITS_PREF, disabled);
}

public static boolean isDeliveryReceiptsForReactionsDisabled(Context context) {
return getBooleanPreference(context, DELIVERY_RECEIPTS_FOR_REACTIONS_PREF, true);
}

public static void setDeliveryReceiptsForReactionsDisabled(Context context, boolean disabled) {
setBooleanPreference(context, DELIVERY_RECEIPTS_FOR_REACTIONS_PREF, disabled);
}

public static boolean isDeliveryReceiptsForDeletesDisabled(Context context) {
return getBooleanPreference(context, DELIVERY_RECEIPTS_FOR_DELETES_PREF, true);
}

public static void setDeliveryReceiptsForDeletesDisabled(Context context, boolean disabled) {
setBooleanPreference(context, DELIVERY_RECEIPTS_FOR_DELETES_PREF, disabled);
}

public static boolean isDeliveryReceiptsForBlockedDisabled(Context context) {
return getBooleanPreference(context, DELIVERY_RECEIPTS_FOR_BLOCKED_PREF, true);
}

public static void setDeliveryReceiptsForBlockedDisabled(Context context, boolean disabled) {
setBooleanPreference(context, DELIVERY_RECEIPTS_FOR_BLOCKED_PREF, disabled);
}

public static boolean isTypingIndicatorsEnabled(Context context) {
return getBooleanPreference(context, TYPING_INDICATORS, false);
}
Expand Down