From 01e75120a72707112b693587d90f7f0af7b8fd04 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 28 Feb 2022 18:35:54 -0500 Subject: [PATCH] Improve network reliability. --- .../app/internal/InternalSettingsFragment.kt | 8 +- .../app/internal/InternalSettingsState.kt | 2 +- .../app/internal/InternalSettingsViewModel.kt | 6 +- .../AdvancedPrivacySettingsFragment.kt | 22 ++ .../advanced/AdvancedPrivacySettingsState.kt | 19 + .../AdvancedPrivacySettingsViewModel.kt | 107 +++++- .../securesms/keyvalue/InternalValues.java | 8 +- .../securesms/keyvalue/SettingsValues.java | 32 ++ .../push/SignalServiceNetworkAccess.java | 344 ------------------ .../push/SignalServiceNetworkAccess.kt | 279 ++++++++++++++ app/src/main/res/values/strings.xml | 27 +- 11 files changed, 484 insertions(+), 370 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index ae0d229a2..ea52fc6c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -198,11 +198,11 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter sectionHeaderPref(R.string.preferences__internal_network) switchPref( - title = DSLSettingsText.from(R.string.preferences__internal_force_censorship), - summary = DSLSettingsText.from(R.string.preferences__internal_force_censorship_description), - isChecked = state.forceCensorship, + title = DSLSettingsText.from(R.string.preferences__internal_allow_censorship_toggle), + summary = DSLSettingsText.from(R.string.preferences__internal_allow_censorship_toggle_description), + isChecked = state.allowCensorshipSetting, onClick = { - viewModel.setForceCensorship(!state.forceCensorship) + viewModel.setAllowCensorshipSetting(!state.allowCensorshipSetting) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt index cb75a837d..1a5d53d8c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt @@ -12,7 +12,7 @@ data class InternalSettingsState( val gv2ignoreP2PChanges: Boolean, val disableAutoMigrationInitiation: Boolean, val disableAutoMigrationNotification: Boolean, - val forceCensorship: Boolean, + val allowCensorshipSetting: Boolean, val callingServer: String, val audioProcessingMethod: CallManager.AudioProcessingMethod, val useBuiltInEmojiSet: Boolean, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt index 8145afde6..495fd779b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt @@ -66,8 +66,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito refresh() } - fun setForceCensorship(enabled: Boolean) { - preferenceDataStore.putBoolean(InternalValues.FORCE_CENSORSHIP, enabled) + fun setAllowCensorshipSetting(enabled: Boolean) { + preferenceDataStore.putBoolean(InternalValues.ALLOW_CENSORSHIP_SETTING, enabled) refresh() } @@ -109,7 +109,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito gv2ignoreP2PChanges = SignalStore.internalValues().gv2IgnoreP2PChanges(), disableAutoMigrationInitiation = SignalStore.internalValues().disableGv1AutoMigrateInitiation(), disableAutoMigrationNotification = SignalStore.internalValues().disableGv1AutoMigrateNotification(), - forceCensorship = SignalStore.internalValues().forcedCensorship(), + allowCensorshipSetting = SignalStore.internalValues().allowChangingCensorshipSetting(), callingServer = SignalStore.internalValues().groupCallingServer(), audioProcessingMethod = SignalStore.internalValues().audioProcessingMethod(), useBuiltInEmojiSet = SignalStore.internalValues().forceBuiltInEmoji(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt index 052d73f52..34c1efc21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsFragment.kt @@ -138,6 +138,28 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences dividerPref() + sectionHeaderPref(R.string.preferences_communication__category_censorship_circumvention) + + val censorshipSummaryResId: Int = when (state.censorshipCircumventionState) { + CensorshipCircumventionState.AVAILABLE -> R.string.preferences_communication__censorship_circumvention_if_enabled_signal_will_attempt_to_circumvent_censorship + CensorshipCircumventionState.AVAILABLE_MANUALLY_DISABLED -> R.string.preferences_communication__censorship_circumvention_you_have_manually_disabled + CensorshipCircumventionState.AVAILABLE_AUTOMATICALLY_ENABLED -> R.string.preferences_communication__censorship_circumvention_has_been_activated_based_on_your_accounts_phone_number + CensorshipCircumventionState.UNAVAILABLE_CONNECTED -> R.string.preferences_communication__censorship_circumvention_is_not_necessary_you_are_already_connected + CensorshipCircumventionState.UNAVAILABLE_NO_INTERNET -> R.string.preferences_communication__censorship_circumvention_can_only_be_activated_when_connected_to_the_internet + } + + switchPref( + title = DSLSettingsText.from(R.string.preferences_communication__censorship_circumvention), + summary = DSLSettingsText.from(censorshipSummaryResId), + isChecked = state.censorshipCircumventionEnabled, + isEnabled = state.censorshipCircumventionState.available, + onClick = { + viewModel.setCensorshipCircumventionEnabled(!state.censorshipCircumventionEnabled) + } + ) + + dividerPref() + sectionHeaderPref(R.string.preferences_communication__category_sealed_sender) switchPref( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsState.kt index 69911d8e1..076c12696 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsState.kt @@ -3,7 +3,26 @@ package org.thoughtcrime.securesms.components.settings.app.privacy.advanced data class AdvancedPrivacySettingsState( val isPushEnabled: Boolean, val alwaysRelayCalls: Boolean, + val censorshipCircumventionState: CensorshipCircumventionState, + val censorshipCircumventionEnabled: Boolean, val showSealedSenderStatusIcon: Boolean, val allowSealedSenderFromAnyone: Boolean, val showProgressSpinner: Boolean ) + +enum class CensorshipCircumventionState(val available: Boolean) { + /** The setting is unavailable because you're connected to the websocket */ + UNAVAILABLE_CONNECTED(false), + + /** The setting is unavailable because you have no network access at all */ + UNAVAILABLE_NO_INTERNET(false), + + /** The setting is available, and the user manually disabled it even though we thought they were censored */ + AVAILABLE_MANUALLY_DISABLED(true), + + /** The setting is available, and it's on because we think the user is censored */ + AVAILABLE_AUTOMATICALLY_ENABLED(true), + + /** The setting is generically available */ + AVAILABLE(true), +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt index 8f85b003c..4a0128731 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/privacy/advanced/AdvancedPrivacySettingsViewModel.kt @@ -4,23 +4,40 @@ import android.content.SharedPreferences import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.CompositeDisposable import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraintObserver import org.thoughtcrime.securesms.jobs.RefreshAttributesJob +import org.thoughtcrime.securesms.keyvalue.SettingsValues import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.util.SingleLiveEvent import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.livedata.Store +import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState class AdvancedPrivacySettingsViewModel( private val sharedPreferences: SharedPreferences, private val repository: AdvancedPrivacySettingsRepository -) : ViewModel() { +) : ViewModel(), NetworkConstraintObserver.NetworkListener { private val store = Store(getState()) private val singleEvents = SingleLiveEvent() val state: LiveData = store.stateLiveData val events: LiveData = singleEvents + val disposables: CompositeDisposable = CompositeDisposable() + + init { + NetworkConstraintObserver.getInstance(ApplicationDependencies.getApplication()).addListener(this) + disposables.add( + ApplicationDependencies.getSignalWebSocket().webSocketState + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { refresh() } + ) + } fun disablePushMessages() { store.update { getState().copy(showProgressSpinner = true) } @@ -58,21 +75,87 @@ class AdvancedPrivacySettingsViewModel( refresh() } + fun setCensorshipCircumventionEnabled(enabled: Boolean) { + SignalStore.settings().setCensorshipCircumventionEnabled(enabled) + ApplicationDependencies.resetNetworkConnectionsAfterProxyChange() + refresh() + } + fun refresh() { store.update { getState().copy(showProgressSpinner = it.showProgressSpinner) } } - private fun getState() = AdvancedPrivacySettingsState( - isPushEnabled = SignalStore.account().isRegistered, - alwaysRelayCalls = TextSecurePreferences.isTurnOnly(ApplicationDependencies.getApplication()), - showSealedSenderStatusIcon = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled( - ApplicationDependencies.getApplication() - ), - allowSealedSenderFromAnyone = TextSecurePreferences.isUniversalUnidentifiedAccess( - ApplicationDependencies.getApplication() - ), - false - ) + override fun onNetworkChanged() { + refresh() + } + + override fun onCleared() { + NetworkConstraintObserver.getInstance(ApplicationDependencies.getApplication()).removeListener(this) + disposables.dispose() + } + + private fun getState(): AdvancedPrivacySettingsState { + val censorshipCircumventionState = getCensorshipCircumventionState() + + return AdvancedPrivacySettingsState( + isPushEnabled = SignalStore.account().isRegistered, + alwaysRelayCalls = TextSecurePreferences.isTurnOnly(ApplicationDependencies.getApplication()), + censorshipCircumventionState = censorshipCircumventionState, + censorshipCircumventionEnabled = getCensorshipCircumventionEnabled(censorshipCircumventionState), + showSealedSenderStatusIcon = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled( + ApplicationDependencies.getApplication() + ), + allowSealedSenderFromAnyone = TextSecurePreferences.isUniversalUnidentifiedAccess( + ApplicationDependencies.getApplication() + ), + false + ) + } + + private fun getCensorshipCircumventionState(): CensorshipCircumventionState { + val countryCode: Int = PhoneNumberFormatter.getLocalCountryCode() + val isCountryCodeCensoredByDefault: Boolean = ApplicationDependencies.getSignalServiceNetworkAccess().isCountryCodeCensoredByDefault(countryCode) + val enabledState: SettingsValues.CensorshipCircumventionEnabled = SignalStore.settings().censorshipCircumventionEnabled + val hasInternet: Boolean = NetworkConstraint.isMet(ApplicationDependencies.getApplication()) + val websocketConnected: Boolean = ApplicationDependencies.getSignalWebSocket().webSocketState.firstOrError().blockingGet() == WebSocketConnectionState.CONNECTED + + return when { + SignalStore.internalValues().allowChangingCensorshipSetting() -> { + CensorshipCircumventionState.AVAILABLE + } + isCountryCodeCensoredByDefault && enabledState == SettingsValues.CensorshipCircumventionEnabled.DISABLED -> { + CensorshipCircumventionState.AVAILABLE_MANUALLY_DISABLED + } + isCountryCodeCensoredByDefault -> { + CensorshipCircumventionState.AVAILABLE_AUTOMATICALLY_ENABLED + } + !hasInternet && enabledState != SettingsValues.CensorshipCircumventionEnabled.ENABLED -> { + CensorshipCircumventionState.UNAVAILABLE_NO_INTERNET + } + websocketConnected && enabledState != SettingsValues.CensorshipCircumventionEnabled.ENABLED -> { + CensorshipCircumventionState.UNAVAILABLE_CONNECTED + } + else -> { + CensorshipCircumventionState.AVAILABLE + } + } + } + + private fun getCensorshipCircumventionEnabled(state: CensorshipCircumventionState): Boolean { + return when (state) { + CensorshipCircumventionState.UNAVAILABLE_CONNECTED, + CensorshipCircumventionState.UNAVAILABLE_NO_INTERNET, + CensorshipCircumventionState.AVAILABLE_MANUALLY_DISABLED -> { + false + } + CensorshipCircumventionState.AVAILABLE_AUTOMATICALLY_ENABLED -> { + true + } + else -> { + SignalStore.settings().censorshipCircumventionEnabled == SettingsValues.CensorshipCircumventionEnabled.ENABLED + } + } + } enum class Event { DISABLE_PUSH_FAILED diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java index 67e595224..914e8b1fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.java @@ -19,7 +19,7 @@ public final class InternalValues extends SignalStoreValues { public static final String GV2_DISABLE_AUTOMIGRATE_INITIATION = "internal.gv2.disable_automigrate_initiation"; public static final String GV2_DISABLE_AUTOMIGRATE_NOTIFICATION = "internal.gv2.disable_automigrate_notification"; public static final String RECIPIENT_DETAILS = "internal.recipient_details"; - public static final String FORCE_CENSORSHIP = "internal.force_censorship"; + public static final String ALLOW_CENSORSHIP_SETTING = "internal.force_censorship"; public static final String FORCE_BUILT_IN_EMOJI = "internal.force_built_in_emoji"; public static final String REMOVE_SENDER_KEY_MINIMUM = "internal.remove_sender_key_minimum"; public static final String DELAY_RESENDS = "internal.delay_resends"; @@ -84,10 +84,10 @@ public final class InternalValues extends SignalStoreValues { } /** - * Force the app to behave as if it is in a country where Signal is censored. + * Allow changing the censorship circumvention setting regardless of network status. */ - public synchronized boolean forcedCensorship() { - return FeatureFlags.internalUser() && getBoolean(FORCE_CENSORSHIP, false); + public synchronized boolean allowChangingCensorshipSetting() { + return FeatureFlags.internalUser() && getBoolean(ALLOW_CENSORSHIP_SETTING, false); } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java index c601f7820..be9e53a15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java @@ -65,6 +65,7 @@ public final class SettingsValues extends SignalStoreValues { private static final String DEFAULT_SMS = "settings.default_sms"; private static final String UNIVERSAL_EXPIRE_TIMER = "settings.universal.expire.timer"; private static final String SENT_MEDIA_QUALITY = "settings.sentMediaQuality"; + private static final String CENSORSHIP_CIRCUMVENTION_ENABLED = "settings.censorshipCircumventionEnabled"; private final SingleLiveEvent onConfigurationSettingChanged = new SingleLiveEvent<>(); @@ -390,6 +391,14 @@ public final class SettingsValues extends SignalStoreValues { return SentMediaQuality.fromCode(getInteger(SENT_MEDIA_QUALITY, SentMediaQuality.STANDARD.getCode())); } + public @NonNull CensorshipCircumventionEnabled getCensorshipCircumventionEnabled() { + return CensorshipCircumventionEnabled.deserialize(getInteger(CENSORSHIP_CIRCUMVENTION_ENABLED, CensorshipCircumventionEnabled.DEFAULT.serialize())); + } + + public void setCensorshipCircumventionEnabled(boolean enabled) { + putInteger(CENSORSHIP_CIRCUMVENTION_ENABLED, enabled ? CensorshipCircumventionEnabled.ENABLED.serialize() : CensorshipCircumventionEnabled.DISABLED.serialize()); + } + private @Nullable Uri getUri(@NonNull String key) { String uri = getString(key, ""); @@ -399,4 +408,27 @@ public final class SettingsValues extends SignalStoreValues { return Uri.parse(uri); } } + + public enum CensorshipCircumventionEnabled { + DEFAULT(0), ENABLED(1), DISABLED(2); + + private final int value; + + CensorshipCircumventionEnabled(int value) { + this.value = value; + } + + public static CensorshipCircumventionEnabled deserialize(int value) { + switch (value) { + case 0: return DEFAULT; + case 1: return ENABLED; + case 2: return DISABLED; + default: throw new IllegalArgumentException("Bad value: " + value); + } + } + + public int serialize() { + return value; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java deleted file mode 100644 index 217553cab..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.java +++ /dev/null @@ -1,344 +0,0 @@ -package org.thoughtcrime.securesms.push; - - -import android.content.Context; - -import androidx.annotation.Nullable; - -import com.annimon.stream.Stream; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.BuildConfig; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.net.CustomDns; -import org.thoughtcrime.securesms.net.DeprecatedClientPreventionInterceptor; -import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor; -import org.thoughtcrime.securesms.net.RemoteDeprecationDetectorInterceptor; -import org.thoughtcrime.securesms.net.SequentialDns; -import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor; -import org.thoughtcrime.securesms.util.Base64; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.push.TrustStore; -import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; -import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl; -import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; -import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; -import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; -import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; -import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import okhttp3.CipherSuite; -import okhttp3.ConnectionSpec; -import okhttp3.Dns; -import okhttp3.Interceptor; -import okhttp3.TlsVersion; - -public class SignalServiceNetworkAccess { - - @SuppressWarnings("unused") - private static final String TAG = Log.tag(SignalServiceNetworkAccess.class); - - public static final Dns DNS = new SequentialDns(Dns.SYSTEM, new CustomDns("1.1.1.1")); - - private static final String COUNTRY_CODE_EGYPT = "+20"; - private static final String COUNTRY_CODE_UAE = "+971"; - private static final String COUNTRY_CODE_OMAN = "+968"; - private static final String COUNTRY_CODE_QATAR = "+974"; - private static final String COUNTRY_CODE_IRAN = "+98"; - private static final String COUNTRY_CODE_CUBA = "+53"; - private static final String COUNTRY_CODE_UZBEKISTAN = "+998"; - - private static final String SERVICE_REFLECTOR_HOST = "europe-west1-signal-cdn-reflector.cloudfunctions.net"; - private static final String SERVICE_FASTLY_HOST = "textsecure-service.whispersystems.org.global.prod.fastly.net"; - private static final String STORAGE_FASTLY_HOST = "storage.signal.org.global.prod.fastly.net"; - private static final String CDN_FASTLY_HOST = "cdn.signal.org.global.prod.fastly.net"; - private static final String CDN2_FASTLY_HOST = "cdn2.signal.org.global.prod.fastly.net"; - private static final String DIRECTORY_FASTLY_HOST = "api.directory.signal.org.global.prod.fastly.net"; - private static final String KBS_FASTLY_HOST = "api.backup.signal.org.global.prod.fastly.net"; - - private static final ConnectionSpec GMAPS_CONNECTION_SPEC = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2) - .cipherSuites(CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384, - CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA) - .supportsTlsExtensions(true) - .build(); - - private static final ConnectionSpec GMAIL_CONNECTION_SPEC = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2) - .cipherSuites(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA) - .supportsTlsExtensions(true) - .build(); - - private static final ConnectionSpec PLAY_CONNECTION_SPEC = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2) - .cipherSuites(CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, - CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, - CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, - CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA) - .supportsTlsExtensions(true) - .build(); - - private static final ConnectionSpec APP_CONNECTION_SPEC = ConnectionSpec.MODERN_TLS; - - private final Map censorshipConfiguration; - private final String[] censoredCountries; - private final SignalServiceConfiguration uncensoredConfiguration; - - public SignalServiceNetworkAccess(Context context) { - - final TrustStore trustStore = new DomainFrontingTrustStore(context); - final SignalServiceUrl baseGoogleService = new SignalServiceUrl("https://www.google.com/service", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalServiceUrl baseAndroidService = new SignalServiceUrl("https://android.clients.google.com/service", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC); - final SignalServiceUrl mapsOneAndroidService = new SignalServiceUrl("https://clients3.google.com/service", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalServiceUrl mapsTwoAndroidService = new SignalServiceUrl("https://clients4.google.com/service", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalServiceUrl mailAndroidService = new SignalServiceUrl("https://inbox.google.com/service", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalServiceUrl egyptGoogleService = new SignalServiceUrl("https://www.google.com.eg/service", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalServiceUrl uaeGoogleService = new SignalServiceUrl("https://www.google.ae/service", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalServiceUrl omanGoogleService = new SignalServiceUrl("https://www.google.com.om/service", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalServiceUrl qatarGoogleService = new SignalServiceUrl("https://www.google.com.qa/service", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalServiceUrl uzbekistanGoogleService = new SignalServiceUrl("https://www.google.co.uz/service", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - - final SignalCdnUrl baseGoogleCdn = new SignalCdnUrl("https://www.google.com/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl baseAndroidCdn = new SignalCdnUrl("https://android.clients.google.com/cdn", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC); - final SignalCdnUrl mapsOneAndroidCdn = new SignalCdnUrl("https://clients3.google.com/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalCdnUrl mapsTwoAndroidCdn = new SignalCdnUrl("https://clients4.google.com/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalCdnUrl mailAndroidCdn = new SignalCdnUrl("https://inbox.google.com/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl egyptGoogleCdn = new SignalCdnUrl("https://www.google.com.eg/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl uaeGoogleCdn = new SignalCdnUrl("https://www.google.ae/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl omanGoogleCdn = new SignalCdnUrl("https://www.google.com.om/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl qatarGoogleCdn = new SignalCdnUrl("https://www.google.com.qa/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl uzbekistanGoogleCdn = new SignalCdnUrl("https://www.google.co.uz/cdn", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - - final SignalCdnUrl baseGoogleCdn2 = new SignalCdnUrl("https://www.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl baseAndroidCdn2 = new SignalCdnUrl("https://android.clients.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC); - final SignalCdnUrl mapsOneAndroidCdn2 = new SignalCdnUrl("https://clients3.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalCdnUrl mapsTwoAndroidCdn2 = new SignalCdnUrl("https://clients4.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalCdnUrl mailAndroidCdn2 = new SignalCdnUrl("https://inbox.google.com/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl egyptGoogleCdn2 = new SignalCdnUrl("https://www.google.com.eg/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl uaeGoogleCdn2 = new SignalCdnUrl("https://www.google.ae/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl omanGoogleCdn2 = new SignalCdnUrl("https://www.google.com.om/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl qatarGoogleCdn2 = new SignalCdnUrl("https://www.google.com.qa/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalCdnUrl uzbekistanGoogleCdn2 = new SignalCdnUrl("https://www.google.co.uz/cdn2", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - - final SignalContactDiscoveryUrl baseGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalContactDiscoveryUrl baseAndroidDiscovery = new SignalContactDiscoveryUrl("https://android.clients.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC); - final SignalContactDiscoveryUrl mapsOneAndroidDiscovery = new SignalContactDiscoveryUrl("https://clients3.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalContactDiscoveryUrl mapsTwoAndroidDiscovery = new SignalContactDiscoveryUrl("https://clients4.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalContactDiscoveryUrl mailAndroidDiscovery = new SignalContactDiscoveryUrl("https://inbox.google.com/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalContactDiscoveryUrl egyptGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.com.eg/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalContactDiscoveryUrl uaeGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.ae/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalContactDiscoveryUrl omanGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.com.om/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalContactDiscoveryUrl qatarGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.com.qa/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalContactDiscoveryUrl uzbekistanGoogleDiscovery = new SignalContactDiscoveryUrl("https://www.google.co.uz/directory", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - - final SignalKeyBackupServiceUrl baseGoogleKbs = new SignalKeyBackupServiceUrl("https://www.google.com/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalKeyBackupServiceUrl baseAndroidKbs = new SignalKeyBackupServiceUrl("https://android.clients.google.com/backup", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC); - final SignalKeyBackupServiceUrl mapsOneAndroidKbs = new SignalKeyBackupServiceUrl("https://clients3.google.com/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalKeyBackupServiceUrl mapsTwoAndroidKbs = new SignalKeyBackupServiceUrl("https://clients4.google.com/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalKeyBackupServiceUrl mailAndroidKbs = new SignalKeyBackupServiceUrl("https://inbox.google.com/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalKeyBackupServiceUrl egyptGoogleKbs = new SignalKeyBackupServiceUrl("https://www.google.com.eg/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalKeyBackupServiceUrl uaeGoogleKbs = new SignalKeyBackupServiceUrl("https://www.google.ae/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalKeyBackupServiceUrl omanGoogleKbs = new SignalKeyBackupServiceUrl("https://www.google.com.om/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalKeyBackupServiceUrl qatarGoogleKbs = new SignalKeyBackupServiceUrl("https://www.google.com.qa/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalKeyBackupServiceUrl uzbekistanGoogleKbs = new SignalKeyBackupServiceUrl("https://www.google.com.qa/backup", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - - final SignalStorageUrl baseGoogleStorage = new SignalStorageUrl("https://www.google.com/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalStorageUrl baseAndroidStorage = new SignalStorageUrl("https://android.clients.google.com/storage", SERVICE_REFLECTOR_HOST, trustStore, PLAY_CONNECTION_SPEC); - final SignalStorageUrl mapsOneAndroidStorage = new SignalStorageUrl("https://clients3.google.com/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalStorageUrl mapsTwoAndroidStorage = new SignalStorageUrl("https://clients4.google.com/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAPS_CONNECTION_SPEC); - final SignalStorageUrl mailAndroidStorage = new SignalStorageUrl("https://inbox.google.com/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalStorageUrl egyptGoogleStorage = new SignalStorageUrl("https://www.google.com.eg/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalStorageUrl uaeGoogleStorage = new SignalStorageUrl("https://www.google.ae/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalStorageUrl omanGoogleStorage = new SignalStorageUrl("https://www.google.com.om/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalStorageUrl qatarGoogleStorage = new SignalStorageUrl("https://www.google.com.qa/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - final SignalStorageUrl uzbekistanGoogleStorage = new SignalStorageUrl("https://www.google.com.qa/storage", SERVICE_REFLECTOR_HOST, trustStore, GMAIL_CONNECTION_SPEC); - - final String[] fastUrls = {"https://cdn.sstatic.net", "https://github.githubassets.com", "https://pinterest.com", "https://open.scdn.co", "https://www.redditstatic.com"}; - - final List interceptors = Arrays.asList(new StandardUserAgentInterceptor(), - new RemoteDeprecationDetectorInterceptor(), - new DeprecatedClientPreventionInterceptor(), - DeviceTransferBlockingInterceptor.getInstance()); - final Optional dns = Optional.of(DNS); - - final byte[] zkGroupServerPublicParams; - - try { - zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS); - } catch (IOException e) { - throw new AssertionError(e); - } - - this.censorshipConfiguration = new HashMap() {{ - - put(COUNTRY_CODE_EGYPT, new SignalServiceConfiguration(new SignalServiceUrl[] {egyptGoogleService, baseGoogleService, baseAndroidService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, - makeSignalCdnUrlMapFor(new SignalCdnUrl[] {egyptGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn, mailAndroidCdn}, - new SignalCdnUrl[] {egyptGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2, mailAndroidCdn2}), - new SignalContactDiscoveryUrl[] {egyptGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, - new SignalKeyBackupServiceUrl[] {egyptGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, - new SignalStorageUrl[] {egyptGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, - new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, - interceptors, - dns, - Optional.absent(), - zkGroupServerPublicParams)); - - put(COUNTRY_CODE_UAE, new SignalServiceConfiguration(new SignalServiceUrl[] {uaeGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, - makeSignalCdnUrlMapFor(new SignalCdnUrl[] {uaeGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn}, - new SignalCdnUrl[] {uaeGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2}), - new SignalContactDiscoveryUrl[] {uaeGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, - new SignalKeyBackupServiceUrl[] {uaeGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, - new SignalStorageUrl[] {uaeGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, - new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, - interceptors, - dns, - Optional.absent(), - zkGroupServerPublicParams)); - - put(COUNTRY_CODE_OMAN, new SignalServiceConfiguration(new SignalServiceUrl[] {omanGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, - makeSignalCdnUrlMapFor(new SignalCdnUrl[] {omanGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn}, - new SignalCdnUrl[] {omanGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2}), - new SignalContactDiscoveryUrl[] {omanGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, - new SignalKeyBackupServiceUrl[] {omanGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, - new SignalStorageUrl[] {omanGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, - new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, - interceptors, - dns, - Optional.absent(), - zkGroupServerPublicParams)); - - - put(COUNTRY_CODE_QATAR, new SignalServiceConfiguration(new SignalServiceUrl[] {qatarGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, - makeSignalCdnUrlMapFor(new SignalCdnUrl[] {qatarGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn}, - new SignalCdnUrl[] {qatarGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2}), - new SignalContactDiscoveryUrl[] {qatarGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, - new SignalKeyBackupServiceUrl[] {qatarGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, - new SignalStorageUrl[] {qatarGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, - new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, - interceptors, - dns, - Optional.absent(), - zkGroupServerPublicParams)); - - put(COUNTRY_CODE_IRAN, new SignalServiceConfiguration(Stream.of(fastUrls).map(url -> new SignalServiceUrl(url, SERVICE_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalServiceUrl[]::new), - makeSignalCdnUrlMapFor(Stream.of(fastUrls).map(url -> new SignalCdnUrl(url, CDN_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalCdnUrl[]::new), - Stream.of(fastUrls).map(url -> new SignalCdnUrl(url, CDN2_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalCdnUrl[]::new)), - Stream.of(fastUrls).map(url -> new SignalContactDiscoveryUrl(url, DIRECTORY_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalContactDiscoveryUrl[]::new), - Stream.of(fastUrls).map(url -> new SignalKeyBackupServiceUrl(url, KBS_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalKeyBackupServiceUrl[]::new), - Stream.of(fastUrls).map(url -> new SignalStorageUrl(url, STORAGE_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalStorageUrl[]::new), - new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, - interceptors, - dns, - Optional.absent(), - zkGroupServerPublicParams)); - - put(COUNTRY_CODE_CUBA, new SignalServiceConfiguration(Stream.of(fastUrls).map(url -> new SignalServiceUrl(url, SERVICE_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalServiceUrl[]::new), - makeSignalCdnUrlMapFor(Stream.of(fastUrls).map(url -> new SignalCdnUrl(url, CDN_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalCdnUrl[]::new), - Stream.of(fastUrls).map(url -> new SignalCdnUrl(url, CDN2_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalCdnUrl[]::new)), - Stream.of(fastUrls).map(url -> new SignalContactDiscoveryUrl(url, DIRECTORY_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalContactDiscoveryUrl[]::new), - Stream.of(fastUrls).map(url -> new SignalKeyBackupServiceUrl(url, KBS_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalKeyBackupServiceUrl[]::new), - Stream.of(fastUrls).map(url -> new SignalStorageUrl(url, STORAGE_FASTLY_HOST, new DomainFrontingDigicertTrustStore(context), APP_CONNECTION_SPEC)).toArray(SignalStorageUrl[]::new), - new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, - interceptors, - dns, - Optional.absent(), - zkGroupServerPublicParams)); - - put(COUNTRY_CODE_UZBEKISTAN, new SignalServiceConfiguration(new SignalServiceUrl[] {uzbekistanGoogleService, baseAndroidService, baseGoogleService, mapsOneAndroidService, mapsTwoAndroidService, mailAndroidService}, - makeSignalCdnUrlMapFor(new SignalCdnUrl[] {uzbekistanGoogleCdn, baseAndroidCdn, baseGoogleCdn, mapsOneAndroidCdn, mapsTwoAndroidCdn, mailAndroidCdn}, - new SignalCdnUrl[] {uzbekistanGoogleCdn2, baseAndroidCdn2, baseGoogleCdn2, mapsOneAndroidCdn2, mapsTwoAndroidCdn2, mailAndroidCdn2}), - new SignalContactDiscoveryUrl[] {uzbekistanGoogleDiscovery, baseGoogleDiscovery, baseAndroidDiscovery, mapsOneAndroidDiscovery, mapsTwoAndroidDiscovery, mailAndroidDiscovery}, - new SignalKeyBackupServiceUrl[] {uzbekistanGoogleKbs, baseGoogleKbs, baseAndroidKbs, mapsOneAndroidKbs, mapsTwoAndroidKbs, mailAndroidKbs}, - new SignalStorageUrl[] {uzbekistanGoogleStorage, baseGoogleStorage, baseAndroidStorage, mapsOneAndroidStorage, mapsTwoAndroidStorage, mailAndroidStorage}, - new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, - interceptors, - dns, - Optional.absent(), - zkGroupServerPublicParams)); - }}; - - this.uncensoredConfiguration = new SignalServiceConfiguration(new SignalServiceUrl[] {new SignalServiceUrl(BuildConfig.SIGNAL_URL, new SignalServiceTrustStore(context))}, - makeSignalCdnUrlMapFor(new SignalCdnUrl[] {new SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, new SignalServiceTrustStore(context))}, - new SignalCdnUrl[] {new SignalCdnUrl(BuildConfig.SIGNAL_CDN2_URL, new SignalServiceTrustStore(context))}), - new SignalContactDiscoveryUrl[] {new SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, new SignalServiceTrustStore(context))}, - new SignalKeyBackupServiceUrl[] { new SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, new SignalServiceTrustStore(context)) }, - new SignalStorageUrl[] {new SignalStorageUrl(BuildConfig.STORAGE_URL, new SignalServiceTrustStore(context))}, - new SignalCdshUrl[] {new SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, new SignalServiceTrustStore(context))}, - interceptors, - dns, - SignalStore.proxy().isProxyEnabled() ? Optional.of(SignalStore.proxy().getProxy()) : Optional.absent(), - zkGroupServerPublicParams); - - this.censoredCountries = this.censorshipConfiguration.keySet().toArray(new String[0]); - } - - public SignalServiceConfiguration getConfiguration() { - String localNumber = SignalStore.account().getE164(); - return getConfiguration(localNumber); - } - - public SignalServiceConfiguration getConfiguration(@Nullable String localNumber) { - if (localNumber == null || SignalStore.proxy().isProxyEnabled()) { - return this.uncensoredConfiguration; - } - - if (SignalStore.internalValues().forcedCensorship()) { - return this.censorshipConfiguration.get(COUNTRY_CODE_IRAN); - } - - for (String censoredRegion : this.censoredCountries) { - if (localNumber.startsWith(censoredRegion)) { - return this.censorshipConfiguration.get(censoredRegion); - } - } - - return this.uncensoredConfiguration; - } - - public boolean isCensored() { - return getConfiguration() != this.uncensoredConfiguration; - } - - public boolean isCensored(String number) { - return getConfiguration(number) != this.uncensoredConfiguration; - } - - private static Map makeSignalCdnUrlMapFor(SignalCdnUrl[] cdn0Urls, SignalCdnUrl[] cdn2Urls) { - Map result = new HashMap<>(); - result.put(0, cdn0Urls); - result.put(2, cdn2Urls); - return Collections.unmodifiableMap(result); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt new file mode 100644 index 000000000..13aa6ccc1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/push/SignalServiceNetworkAccess.kt @@ -0,0 +1,279 @@ +package org.thoughtcrime.securesms.push + +import android.content.Context +import okhttp3.CipherSuite +import okhttp3.ConnectionSpec +import okhttp3.Dns +import okhttp3.Interceptor +import okhttp3.TlsVersion +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.BuildConfig +import org.thoughtcrime.securesms.keyvalue.SettingsValues +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.net.CustomDns +import org.thoughtcrime.securesms.net.DeprecatedClientPreventionInterceptor +import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor +import org.thoughtcrime.securesms.net.RemoteDeprecationDetectorInterceptor +import org.thoughtcrime.securesms.net.SequentialDns +import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor +import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter +import org.thoughtcrime.securesms.util.Base64 +import org.whispersystems.libsignal.util.guava.Optional +import org.whispersystems.signalservice.api.push.TrustStore +import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl +import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl +import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl +import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration +import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl +import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl +import java.io.IOException + +/** + * Provides a [SignalServiceConfiguration] to be used with our service layer. + * If you're looking for a place to start, look at [getConfiguration]. + */ +class SignalServiceNetworkAccess(context: Context) { + companion object { + private val TAG = Log.tag(SignalServiceNetworkAccess::class.java) + + @JvmField + val DNS: Dns = SequentialDns(Dns.SYSTEM, CustomDns("1.1.1.1")) + + private const val COUNTRY_CODE_EGYPT = 20 + private const val COUNTRY_CODE_UAE = 971 + private const val COUNTRY_CODE_OMAN = 968 + private const val COUNTRY_CODE_QATAR = 974 + private const val COUNTRY_CODE_IRAN = 98 + private const val COUNTRY_CODE_CUBA = 53 + private const val COUNTRY_CODE_UZBEKISTAN = 998 + private const val COUNTRY_CODE_UKRAINE = 380 + + private const val G_HOST = "europe-west1-signal-cdn-reflector.cloudfunctions.net" + private const val F_SERVICE_HOST = "textsecure-service.whispersystems.org.global.prod.fastly.net" + private const val F_STORAGE_HOST = "storage.signal.org.global.prod.fastly.net" + private const val F_CDN_HOST = "cdn.signal.org.global.prod.fastly.net" + private const val F_CDN2_HOST = "cdn2.signal.org.global.prod.fastly.net" + private const val F_DIRECTORY_HOST = "api.directory.signal.org.global.prod.fastly.net" + private const val F_KBS_HOST = "api.backup.signal.org.global.prod.fastly.net" + + private val GMAPS_CONNECTION_SPEC = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2) + .cipherSuites( + CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA + ) + .supportsTlsExtensions(true) + .build() + + private val GMAIL_CONNECTION_SPEC = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2) + .cipherSuites( + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA + ) + .supportsTlsExtensions(true) + .build() + + private val PLAY_CONNECTION_SPEC = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2) + .cipherSuites( + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, + CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA + ) + .supportsTlsExtensions(true) + .build() + + private val APP_CONNECTION_SPEC = ConnectionSpec.MODERN_TLS + } + + private val serviceTrustStore: TrustStore = SignalServiceTrustStore(context) + private val gTrustStore: TrustStore = DomainFrontingTrustStore(context) + private val fTrustStore: TrustStore = DomainFrontingDigicertTrustStore(context) + + private val interceptors: List = listOf( + StandardUserAgentInterceptor(), + RemoteDeprecationDetectorInterceptor(), + DeprecatedClientPreventionInterceptor(), + DeviceTransferBlockingInterceptor.getInstance() + ) + + private val zkGroupServerPublicParams: ByteArray = try { + Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS) + } catch (e: IOException) { + throw AssertionError(e) + } + + private val baseGHostConfigs: List = listOf( + HostConfig("https://www.google.com", G_HOST, GMAIL_CONNECTION_SPEC), + HostConfig("https://android.clients.google.com", G_HOST, PLAY_CONNECTION_SPEC), + HostConfig("https://clients3.google.com", G_HOST, GMAPS_CONNECTION_SPEC), + HostConfig("https://clients4.google.com", G_HOST, GMAPS_CONNECTION_SPEC), + HostConfig("https://inbox.google.com", G_HOST, GMAIL_CONNECTION_SPEC), + ) + + private val fUrls = arrayOf("https://cdn.sstatic.net", "https://github.githubassets.com", "https://pinterest.com", "https://open.scdn.co", "https://www.redditstatic.com") + + private val fConfig: SignalServiceConfiguration = SignalServiceConfiguration( + fUrls.map { SignalServiceUrl(it, F_SERVICE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), + mapOf( + 0 to fUrls.map { SignalCdnUrl(it, F_CDN_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), + 2 to fUrls.map { SignalCdnUrl(it, F_CDN2_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), + ), + fUrls.map { SignalContactDiscoveryUrl(it, F_DIRECTORY_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), + fUrls.map { SignalKeyBackupServiceUrl(it, F_KBS_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), + fUrls.map { SignalStorageUrl(it, F_STORAGE_HOST, fTrustStore, APP_CONNECTION_SPEC) }.toTypedArray(), + arrayOf(SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, serviceTrustStore)), + interceptors, + Optional.of(DNS), + Optional.absent(), + zkGroupServerPublicParams + ) + + private val censorshipConfiguration: Map = mapOf( + COUNTRY_CODE_EGYPT to buildGConfiguration( + listOf(HostConfig("https://www.google.com.eg", G_HOST, GMAIL_CONNECTION_SPEC)) + baseGHostConfigs, + ), + COUNTRY_CODE_UAE to buildGConfiguration( + listOf(HostConfig("https://www.google.ae", G_HOST, GMAIL_CONNECTION_SPEC)) + baseGHostConfigs, + ), + COUNTRY_CODE_OMAN to buildGConfiguration( + listOf(HostConfig("https://www.google.com.om", G_HOST, GMAIL_CONNECTION_SPEC)) + baseGHostConfigs, + ), + COUNTRY_CODE_QATAR to buildGConfiguration( + listOf(HostConfig("https://www.google.com.qa", G_HOST, GMAIL_CONNECTION_SPEC)) + baseGHostConfigs, + ), + COUNTRY_CODE_UZBEKISTAN to buildGConfiguration( + listOf(HostConfig("https://www.google.co.uz", G_HOST, GMAIL_CONNECTION_SPEC)) + baseGHostConfigs, + ), + COUNTRY_CODE_UKRAINE to buildGConfiguration( + listOf(HostConfig("https://www.google.com.ua", G_HOST, GMAIL_CONNECTION_SPEC)) + baseGHostConfigs, + ), + COUNTRY_CODE_IRAN to fConfig, + COUNTRY_CODE_CUBA to fConfig, + ) + + private val defaultCensoredConfiguration: SignalServiceConfiguration = buildGConfiguration(baseGHostConfigs) + + private val defaultCensoredCountryCodes: Set = setOf( + COUNTRY_CODE_EGYPT, + COUNTRY_CODE_UAE, + COUNTRY_CODE_OMAN, + COUNTRY_CODE_QATAR, + COUNTRY_CODE_IRAN, + COUNTRY_CODE_CUBA, + COUNTRY_CODE_UZBEKISTAN, + ) + + private val uncensoredConfiguration: SignalServiceConfiguration = SignalServiceConfiguration( + arrayOf(SignalServiceUrl(BuildConfig.SIGNAL_URL, serviceTrustStore)), + mapOf( + 0 to arrayOf(SignalCdnUrl(BuildConfig.SIGNAL_CDN_URL, serviceTrustStore)), + 2 to arrayOf(SignalCdnUrl(BuildConfig.SIGNAL_CDN2_URL, serviceTrustStore)) + ), + arrayOf(SignalContactDiscoveryUrl(BuildConfig.SIGNAL_CONTACT_DISCOVERY_URL, serviceTrustStore)), + arrayOf(SignalKeyBackupServiceUrl(BuildConfig.SIGNAL_KEY_BACKUP_URL, serviceTrustStore)), + arrayOf(SignalStorageUrl(BuildConfig.STORAGE_URL, serviceTrustStore)), + arrayOf(SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, serviceTrustStore)), + interceptors, + Optional.of(DNS), + if (SignalStore.proxy().isProxyEnabled) Optional.of(SignalStore.proxy().proxy) else Optional.absent(), + zkGroupServerPublicParams + ) + + fun getConfiguration(): SignalServiceConfiguration { + return getConfiguration(SignalStore.account().e164) + } + + fun getConfiguration(localNumber: String?): SignalServiceConfiguration { + if (localNumber == null || SignalStore.proxy().isProxyEnabled) { + return uncensoredConfiguration + } + + val countryCode: Int = PhoneNumberFormatter.getLocalCountryCode() + + return when (SignalStore.settings().censorshipCircumventionEnabled) { + SettingsValues.CensorshipCircumventionEnabled.ENABLED -> { + censorshipConfiguration[countryCode] ?: defaultCensoredConfiguration + } + SettingsValues.CensorshipCircumventionEnabled.DISABLED -> { + uncensoredConfiguration + } + SettingsValues.CensorshipCircumventionEnabled.DEFAULT -> { + if (defaultCensoredCountryCodes.contains(countryCode)) { + censorshipConfiguration[countryCode] ?: defaultCensoredConfiguration + } else { + uncensoredConfiguration + } + } + } + } + + fun isCensored(): Boolean { + return isCensored(SignalStore.account().e164) + } + + fun isCensored(number: String?): Boolean { + return getConfiguration(number) != uncensoredConfiguration + } + + fun isCountryCodeCensoredByDefault(countryCode: Int): Boolean { + return defaultCensoredCountryCodes.contains(countryCode) + } + + private fun buildGConfiguration( + hostConfigs: List + ): SignalServiceConfiguration { + val serviceUrls: Array = hostConfigs.map { SignalServiceUrl("${it.baseUrl}/service", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() + val cdnUrls: Array = hostConfigs.map { SignalCdnUrl("${it.baseUrl}/cdn", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() + val cdn2Urls: Array = hostConfigs.map { SignalCdnUrl("${it.baseUrl}/cdn2", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() + val cdsUrls: Array = hostConfigs.map { SignalContactDiscoveryUrl("${it.baseUrl}/directory", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() + val kbsUrls: Array = hostConfigs.map { SignalKeyBackupServiceUrl("${it.baseUrl}/backup", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() + val storageUrls: Array = hostConfigs.map { SignalStorageUrl("${it.baseUrl}/storage", it.host, gTrustStore, it.connectionSpec) }.toTypedArray() + val cdshUrls: Array = listOf(SignalCdshUrl(BuildConfig.SIGNAL_CDSH_URL, serviceTrustStore)).toTypedArray() + + return SignalServiceConfiguration( + serviceUrls, + mapOf( + 0 to cdnUrls, + 2 to cdn2Urls + ), + cdsUrls, + kbsUrls, + storageUrls, + cdshUrls, + interceptors, + Optional.of(DNS), + Optional.absent(), + zkGroupServerPublicParams + ) + } + + private data class HostConfig(val baseUrl: String, val host: String, val connectionSpec: ConnectionSpec) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f05de991e..bc243faee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -152,6 +152,16 @@ Invite a contact to join Signal Search + + + Turn off censorship circumvention? + + You can now connect to the Signal service directly for a better experience. + + Turn off + + No thanks + Remove Remove profile photo? @@ -2536,6 +2546,19 @@ Message font size Contact joined Signal Priority + + Censorship circumvention + + Censorship circumvention + If enabled, Signal will attempt to circumvent censorship. Do not enable this feature unless you are in a location where Signal is censored. + + Censorship circumvention has been activated based on your account\'s phone number. + + You have manually disabled censorship circumvention. + + Censorship circumvention is not necessary; you are already connected to the Signal service. + + Censorship circumvention can only be activated when connected to the internet. Sealed Sender Display indicators Show a status icon when you select "Message details" on messages that were delivered using sealed sender. @@ -2600,8 +2623,8 @@ Overwrite remote data Forces remote storage to match the local device state. Network - Force censorship - Force the app to behave as if it is in a country where Signal is censored. + Allow censorship circumvention toggle + Allow changing the censorship circumvention toggle regardless of network connectivity. Conversations and Shortcuts Emoji Use built-in emoji set