From a11888ff715251d9658ad5d49e389b16c4e82e56 Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Fri, 30 Jan 2026 13:48:01 -0500 Subject: [PATCH] Allow opting out of key transparency. --- .../AdvancedPrivacySettingsFragment.kt | 44 ++++++++++++++++++- .../advanced/AdvancedPrivacySettingsState.kt | 3 +- .../AdvancedPrivacySettingsViewModel.kt | 21 ++++++++- .../conversation/v2/ConversationFragment.kt | 2 +- .../securesms/database/RecipientTable.kt | 8 ++++ .../securesms/keyvalue/SettingsValues.java | 9 ++++ .../storage/AccountRecordProcessor.kt | 1 + .../securesms/storage/StorageSyncHelper.kt | 6 +++ .../securesms/verify/VerifyDisplayFragment.kt | 3 +- app/src/main/res/values/strings.xml | 4 ++ .../src/main/protowire/StorageService.proto | 2 + 11 files changed, 98 insertions(+), 5 deletions(-) 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 92d11c54fc..123c17183f 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 @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.compose.ComposeFragment import org.thoughtcrime.securesms.compose.rememberStatusBarColorNestedScrollModifier import org.thoughtcrime.securesms.util.CommunicationActions +import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.viewModel /** @@ -152,6 +153,17 @@ class AdvancedPrivacySettingsFragment : ComposeFragment() { getString(R.string.AdvancedPrivacySettingsFragment__sealed_sender_link) ) } + + override fun onAllowAutomaticVerificationChanged(enabled: Boolean) { + viewModel.setAllowAutomaticVerification(enabled) + } + + override fun onAutomaticVerificationLearnMoreClick() { + CommunicationActions.openBrowserLink( + requireContext(), + getString(R.string.verify_display_fragment__link) + ) + } } } @@ -163,6 +175,8 @@ private interface AdvancedPrivacySettingsCallbacks { fun onShowStatusIconForSealedSenderChanged(enabled: Boolean) = Unit fun onAllowSealedSenderFromAnyoneChanged(enabled: Boolean) = Unit fun onSealedSenderLearnMoreClick() = Unit + fun onAutomaticVerificationLearnMoreClick() = Unit + fun onAllowAutomaticVerificationChanged(enabled: Boolean) = Unit object Empty : AdvancedPrivacySettingsCallbacks } @@ -284,6 +298,33 @@ private fun AdvancedPrivacySettingsScreen( text = sealedSenderSummary ) } + + if (RemoteConfig.keyTransparency) { + item { + Dividers.Default() + } + + item { + val label = buildAnnotatedString { + append(stringResource(R.string.preferences_automatic_key_verification_body)) + append(" ") + withLink( + LinkAnnotation.Clickable("learn-more", linkInteractionListener = { + callbacks.onAutomaticVerificationLearnMoreClick() + }) + ) { + append(stringResource(R.string.LearnMoreTextView_learn_more)) + } + } + + Rows.ToggleRow( + checked = state.allowAutomaticKeyVerification, + text = AnnotatedString(stringResource(R.string.preferences_automatic_key_verification)), + label = label, + onCheckChanged = callbacks::onAllowAutomaticVerificationChanged + ) + } + } } } } @@ -300,7 +341,8 @@ private fun AdvancedPrivacySettingsScreenPreview() { censorshipCircumventionEnabled = false, showSealedSenderStatusIcon = false, allowSealedSenderFromAnyone = false, - showProgressSpinner = false + showProgressSpinner = false, + allowAutomaticKeyVerification = false ), callbacks = AdvancedPrivacySettingsCallbacks.Empty ) 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 3f5692d2b7..063b44c358 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 @@ -7,7 +7,8 @@ data class AdvancedPrivacySettingsState( val censorshipCircumventionEnabled: Boolean, val showSealedSenderStatusIcon: Boolean, val allowSealedSenderFromAnyone: Boolean, - val showProgressSpinner: Boolean + val showProgressSpinner: Boolean, + val allowAutomaticKeyVerification: Boolean ) enum class CensorshipCircumventionState(val available: Boolean) { 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 fef62e3700..be0a9130ca 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 @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.privacy.advanced import android.content.SharedPreferences import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.CompositeDisposable import kotlinx.coroutines.flow.MutableSharedFlow @@ -9,12 +10,17 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.signal.core.util.concurrent.SignalDispatchers +import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobs.RefreshAttributesJob import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob import org.thoughtcrime.securesms.keyvalue.SettingsValues import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.TextSecurePreferences import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState @@ -63,6 +69,18 @@ class AdvancedPrivacySettingsViewModel( refresh() } + fun setAllowAutomaticVerification(enabled: Boolean) { + SignalStore.settings.automaticVerificationEnabled = enabled + refresh() + viewModelScope.launch(SignalDispatchers.IO) { + if (!enabled) { + SignalDatabase.recipients.clearAllKeyTransparencyData() + } + SignalDatabase.recipients.markNeedsSync(Recipient.self().id) + StorageSyncHelper.scheduleSyncForDataChange() + } + } + fun refresh() { store.update { getState().copy(showProgressSpinner = it.showProgressSpinner) } } @@ -85,7 +103,8 @@ class AdvancedPrivacySettingsViewModel( allowSealedSenderFromAnyone = TextSecurePreferences.isUniversalUnidentifiedAccess( AppDependencies.application ), - false + showProgressSpinner = false, + allowAutomaticKeyVerification = SignalStore.settings.automaticVerificationEnabled ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 65a3482f6d..a877a57f35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -1418,7 +1418,7 @@ class ConversationFragment : } private fun presentVerifyAutomaticallySheet() { - if (RemoteConfig.keyTransparency && !SignalStore.uiHints.hasSeenVerifyAutomaticallySheet() && viewModel.recipientSnapshot?.isIndividual == true) { + if (RemoteConfig.keyTransparency && SignalStore.settings.automaticVerificationEnabled && !SignalStore.uiHints.hasSeenVerifyAutomaticallySheet() && viewModel.recipientSnapshot?.isIndividual == true) { VerifyAutomaticallyEducationSheet.show(parentFragmentManager) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 8905842e30..edd588cc72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -4048,6 +4048,14 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da .run() } + fun clearAllKeyTransparencyData() { + writableDatabase + .update(TABLE_NAME) + .values(KEY_TRANSPARENCY_DATA to null) + .where("$KEY_TRANSPARENCY_DATA IS NOT NULL") + .run() + } + /** * Will update the database with the content values you specified. It will make an intelligent * query such that this will only return true if a row was *actually* updated. 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 53bf403012..6616ad7115 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java @@ -75,6 +75,7 @@ public final class SettingsValues extends SignalStoreValues { private static final String PASSPHRASE_TIMEOUT = "settings.passphrase.timeout"; private static final String SCREEN_LOCK_ENABLED = "settings.screen.lock.enabled"; private static final String SCREEN_LOCK_TIMEOUT = "settings.screen.lock.timeout"; + private static final String AUTOMATIC_VERIFICATION_ENABLED = "settings.automatic.verification.enabled"; public static final int BACKUP_DEFAULT_HOUR = 2; public static final int BACKUP_DEFAULT_MINUTE = 0; @@ -560,6 +561,14 @@ public final class SettingsValues extends SignalStoreValues { return getLong(SCREEN_LOCK_TIMEOUT, 0); } + public boolean getAutomaticVerificationEnabled() { + return getBoolean(AUTOMATIC_VERIFICATION_ENABLED, true); + } + + public void setAutomaticVerificationEnabled(boolean enabled) { + putBoolean(AUTOMATIC_VERIFICATION_ENABLED, enabled); + } + private @Nullable Uri getUri(@NonNull String key) { String uri = getString(key, ""); diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt index 6a4d0825a4..5651faf602 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt @@ -138,6 +138,7 @@ class AccountRecordProcessor( usernameLink = remote.proto.usernameLink notificationProfileManualOverride = remote.proto.notificationProfileManualOverride backupTier = local.proto.backupTier ?: remote.proto.backupTier + automaticKeyVerificationDisabled = remote.proto.automaticKeyVerificationDisabled safeSetPayments(payments?.enabled == true, payments?.entropy?.toByteArray()) safeSetSubscriber(donationSubscriberId, donationSubscriberCurrencyCode) diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt index 67c7a4020d..8378c33d68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt @@ -195,6 +195,7 @@ object StorageSyncHelper { } safeSetPayments(SignalStore.payments.mobileCoinPaymentsEnabled(), Optional.ofNullable(SignalStore.payments.paymentsEntropy).map { obj: Entropy -> obj.bytes }.orElse(null)) + automaticKeyVerificationDisabled = !SignalStore.settings.automaticVerificationEnabled } return accountRecord.toSignalAccountRecord(StorageId.forAccount(storageId)).toSignalStorageRecord() @@ -258,6 +259,11 @@ object StorageSyncHelper { SignalStore.story.userHasSeenGroupStoryEducationSheet = update.new.proto.hasSeenGroupStoryEducationSheet SignalStore.uiHints.setHasCompletedUsernameOnboarding(update.new.proto.hasCompletedUsernameOnboarding) + if (SignalStore.settings.automaticVerificationEnabled && update.new.proto.automaticKeyVerificationDisabled) { + SignalDatabase.recipients.clearAllKeyTransparencyData() + } + SignalStore.settings.automaticVerificationEnabled = !update.new.proto.automaticKeyVerificationDisabled + if (update.new.proto.storyViewReceiptsEnabled == OptionalBool.UNSET) { SignalStore.story.viewedReceiptsEnabled = update.new.proto.readReceipts } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt index 380025eeaf..1cf1722a2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyDisplayFragment.kt @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.components.ViewBinderDelegate import org.thoughtcrime.securesms.components.verify.SafetyNumberQrView.Companion.getSegments import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable import org.thoughtcrime.securesms.databinding.VerifyDisplayFragmentBinding +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.util.RemoteConfig @@ -74,7 +75,7 @@ class VerifyDisplayFragment : Fragment() { updateVerifyButton(requireArguments().getBoolean(VERIFIED_STATE, false), false) - binding.automaticVerification.visible = RemoteConfig.keyTransparency + binding.automaticVerification.visible = RemoteConfig.keyTransparency && SignalStore.settings.automaticVerificationEnabled binding.safetyQrView.verifyButton.setOnClickListener { updateVerifyButton(!currentVerifiedState, true) } binding.toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() } binding.toolbar.setTitle(R.string.AndroidManifest__verify_safety_number) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1fa1abb6fb..ec055207e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4247,6 +4247,10 @@ Compact The app will restart to apply the new language setting. + + Automatic key verification + + When enabled, Signal will attempt to automatically verify the encryption of 1:1 chats. Customize option diff --git a/lib/libsignal-service/src/main/protowire/StorageService.proto b/lib/libsignal-service/src/main/protowire/StorageService.proto index f49fa2dc68..95ec8454d6 100644 --- a/lib/libsignal-service/src/main/protowire/StorageService.proto +++ b/lib/libsignal-service/src/main/protowire/StorageService.proto @@ -294,6 +294,8 @@ message AccountRecord { optional AvatarColor avatarColor = 42; BackupTierHistory backupTierHistory = 43; NotificationProfileManualOverride notificationProfileManualOverride = 44; + bool notificationProfileSyncDisabled = 45; + bool automaticKeyVerificationDisabled = 46; } message StoryDistributionListRecord {