From 04a8996348888e43bd540299899dc5bdf1d121b3 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 9 Jul 2020 16:04:30 -0700 Subject: [PATCH] Add the ability to opt out of PINs. --- .../ConversationListFragment.java | 1 + .../securesms/jobs/RefreshAttributesJob.java | 2 +- .../securesms/keyvalue/KbsValues.java | 29 ++++ .../securesms/lock/v2/BaseKbsPinFragment.java | 69 ++++++++ .../lock/v2/ConfirmKbsPinFragment.java | 9 -- .../lock/v2/CreateKbsPinFragment.java | 1 - .../securesms/lock/v2/KbsSplashFragment.java | 59 ++++++- .../megaphone/PinsForAllSchedule.java | 4 + .../megaphone/SignalPinReminderSchedule.java | 4 + .../securesms/pin/PinOptOutDialog.java | 88 ++++++++++ .../pin/PinRestoreEntryFragment.java | 2 - .../thoughtcrime/securesms/pin/PinState.java | 153 ++++++++++-------- .../AdvancedPinPreferenceFragment.java | 95 +++++++++++ .../AdvancedPreferenceFragment.java | 19 +++ .../AppProtectionPreferenceFragment.java | 3 +- .../main/res/layout/base_kbs_pin_fragment.xml | 7 + .../main/res/layout/kbs_splash_fragment.xml | 7 + .../res/layout/pin_restore_entry_fragment.xml | 1 - app/src/main/res/menu/pin_skip.xml | 15 ++ app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/strings.xml | 23 ++- app/src/main/res/values/themes.xml | 4 + app/src/main/res/xml/preferences_advanced.xml | 32 ++-- .../main/res/xml/preferences_advanced_pin.xml | 22 +++ 24 files changed, 550 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/pin/PinOptOutDialog.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinPreferenceFragment.java create mode 100644 app/src/main/res/menu/pin_skip.xml create mode 100644 app/src/main/res/xml/preferences_advanced_pin.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 0d017ca876..c66e648374 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -100,6 +100,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.events.ReminderUpdateEvent; import org.thoughtcrime.securesms.insights.InsightsLauncher; import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mediasend.MediaSendActivity; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index f27310ea25..a9f05a5727 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -70,7 +70,7 @@ public class RefreshAttributesJob extends BaseJob { registrationLockV1 = TextSecurePreferences.getDeprecatedV1RegistrationLockPin(context); } - SignalServiceProfile.Capabilities capabilities = AppCapabilities.getCapabilities(kbsValues.hasPin()); + SignalServiceProfile.Capabilities capabilities = AppCapabilities.getCapabilities(kbsValues.hasPin() && !kbsValues.hasOptedOut()); Log.i(TAG, "Calling setAccountAttributes() reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2) + ", pin? " + kbsValues.hasPin() + "\n Capabilities:" + "\n Storage? " + capabilities.isStorage() + diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java index ca5ebb295c..94ce5112c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java @@ -20,6 +20,7 @@ public final class KbsValues extends SignalStoreValues { private static final String PIN = "kbs.pin"; private static final String LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash"; private static final String LAST_CREATE_FAILED_TIMESTAMP = "kbs.last_create_failed_timestamp"; + public static final String OPTED_OUT = "kbs.opted_out"; KbsValues(KeyValueStore store) { super(store); @@ -41,6 +42,7 @@ public final class KbsValues extends SignalStoreValues { .remove(LOCK_LOCAL_PIN_HASH) .remove(PIN) .remove(LAST_CREATE_FAILED_TIMESTAMP) + .remove(OPTED_OUT) .commit(); } @@ -142,6 +144,33 @@ public final class KbsValues extends SignalStoreValues { return getLocalPinHash() != null; } + /** + * Should only be called by {@link org.thoughtcrime.securesms.pin.PinState}. + */ + public synchronized void optIn() { + putBoolean(OPTED_OUT, false); + } + + /** + * Should only be called by {@link org.thoughtcrime.securesms.pin.PinState}. + */ + public synchronized void optOut() { + putBoolean(OPTED_OUT, true); + } + + /** + * Should only be called by {@link org.thoughtcrime.securesms.pin.PinState}. + */ + public synchronized void resetMasterKey() { + getStore().beginWrite() + .remove(MASTER_KEY) + .apply(); + } + + public synchronized boolean hasOptedOut() { + return getBoolean(OPTED_OUT, false); + } + public synchronized @Nullable TokenResponse getRegistrationLockTokenResponse() { String token = getStore().getString(TOKEN_RESPONSE, null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java index 0af0b2409b..7d16bfc227 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/BaseKbsPinFragment.java @@ -1,9 +1,13 @@ package org.thoughtcrime.securesms.lock.v2; +import android.content.Intent; import android.os.Bundle; import android.text.InputType; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; @@ -13,11 +17,16 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; import com.airbnb.lottie.LottieAnimationView; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.pin.PinOptOutDialog; +import org.thoughtcrime.securesms.pin.PinState; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.text.AfterTextChanged; import org.thoughtcrime.securesms.util.views.LearnMoreTextView; @@ -34,6 +43,12 @@ abstract class BaseKbsPinFragment extends private LottieAnimationView lottieEnd; private ViewModel viewModel; + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + } + @Override public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @@ -63,6 +78,10 @@ abstract class BaseKbsPinFragment extends CommunicationActions.openBrowserLink(requireContext(), getString(R.string.BaseKbsPinFragment__learn_more_url)); }); + Toolbar toolbar = view.findViewById(R.id.kbs_pin_toolbar); + ((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar); + ((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(null); + initializeListeners(); } @@ -73,6 +92,34 @@ abstract class BaseKbsPinFragment extends input.requestFocus(); } + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + inflater.inflate(R.menu.pin_skip, menu); + } + + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + if (RegistrationLockUtil.userHasRegistrationLock(requireContext()) || + SignalStore.kbsValues().hasPin()) + { + menu.clear(); + } + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_pin_learn_more: + onLearnMore(); + return true; + case R.id.menu_pin_skip: + onPinSkipped(); + return true; + } + + return false; + } + protected abstract ViewModel initializeViewModel(); protected abstract void initializeViewStates(); @@ -109,6 +156,15 @@ abstract class BaseKbsPinFragment extends return confirm; } + protected void closeNavGraphBranch() { + Intent activityIntent = requireActivity().getIntent(); + if (activityIntent != null && activityIntent.hasExtra("next_intent")) { + startActivity(activityIntent.getParcelableExtra("next_intent")); + } + + requireActivity().finish(); + } + private void initializeViews(@NonNull View view) { title = view.findViewById(R.id.edit_kbs_pin_title); description = view.findViewById(R.id.edit_kbs_pin_description); @@ -152,4 +208,17 @@ abstract class BaseKbsPinFragment extends return R.string.BaseKbsPinFragment__create_alphanumeric_pin; } } + + private void onLearnMore() { + CommunicationActions.openBrowserLink(requireContext(), getString(R.string.KbsSplashFragment__learn_more_link)); + } + + private void onPinSkipped() { + PinOptOutDialog.showForSkip(requireContext(), + this::closeNavGraphBranch, + () -> { + PinState.onPinCreateFailure(); + closeNavGraphBranch(); + }); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java index 7b60f959e6..d7f44a9861 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/lock/v2/ConfirmKbsPinFragment.java @@ -186,15 +186,6 @@ public class ConfirmKbsPinFragment extends BaseKbsPinFragment requireActivity().finish(), + () -> { + PinState.onPinCreateFailure(); + requireActivity().finish(); + }); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java index 3dba83d357..12cac35b11 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/PinsForAllSchedule.java @@ -44,6 +44,10 @@ class PinsForAllSchedule implements MegaphoneSchedule { } private static boolean isEnabled() { + if (SignalStore.kbsValues().hasOptedOut()) { + return false; + } + if (SignalStore.kbsValues().hasPin()) { return false; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java index 50f6269ad5..9ad7a6b3cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/SignalPinReminderSchedule.java @@ -9,6 +9,10 @@ final class SignalPinReminderSchedule implements MegaphoneSchedule { @Override public boolean shouldDisplay(int seenCount, long lastSeen, long firstVisible, long currentTime) { + if (SignalStore.kbsValues().hasOptedOut()) { + return false; + } + if (!SignalStore.kbsValues().hasPin()) { return false; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinOptOutDialog.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinOptOutDialog.java new file mode 100644 index 0000000000..cbd2f79fd3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinOptOutDialog.java @@ -0,0 +1,88 @@ +package org.thoughtcrime.securesms.pin; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.util.ThemeUtil; +import org.thoughtcrime.securesms.util.concurrent.SimpleTask; +import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; + +import java.io.IOException; + +public final class PinOptOutDialog { + + private static final String TAG = Log.tag(PinOptOutDialog.class); + + public static void showForSkip(@NonNull Context context, @NonNull Runnable onSuccess, @NonNull Runnable onFailed) { + show(context, + R.string.PinOptOutDialog_warning, + R.string.PinOptOutDialog_skipping_pin_creation_will_create_a_hidden_high_entropy_pin, + R.string.PinOptOutDialog_skip_pin_creation, + true, + onSuccess, + onFailed); + } + + public static void showForOptOut(@NonNull Context context, @NonNull Runnable onSuccess, @NonNull Runnable onFailed) { + show(context, + R.string.PinOptOutDialog_warning, + R.string.PinOptOutDialog_disabling_pins_will_create_a_hidden_high_entropy_pin, + R.string.PinOptOutDialog_disable_pin, + false, + onSuccess, + onFailed); + } + + private static void show(@NonNull Context context, + @StringRes int titleRes, + @StringRes int bodyRes, + @StringRes int buttonRes, + boolean skip, + @NonNull Runnable onSuccess, + @NonNull Runnable onFailed) + { + AlertDialog dialog = new AlertDialog.Builder(context) + .setTitle(titleRes) + .setMessage(bodyRes) + .setCancelable(true) + .setPositiveButton(buttonRes, (d, which) -> { + d.dismiss(); + AlertDialog progress = SimpleProgressDialog.show(context); + + SimpleTask.run(() -> { + try { + if (skip) { + PinState.onPinCreationSkipped(context); + } else { + PinState.onPinOptOut(context); + } + return true; + } catch (IOException | UnauthenticatedResponseException e) { + Log.w(TAG, e); + return false; + } + }, success -> { + if (success) { + onSuccess.run(); + } else { + onFailed.run(); + } + progress.dismiss(); + }); + }) + .setNegativeButton(android.R.string.cancel, (d, which) -> d.dismiss()) + .create(); + + dialog.setOnShowListener(dialogInterface -> { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ThemeUtil.getThemedColor(context, R.attr.dangerous_button_color)); + }); + + dialog.show(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java index e7dcb61320..8529844d42 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreEntryFragment.java @@ -127,7 +127,6 @@ public class PinRestoreEntryFragment extends LoggingFragment { errorLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin); helpButton.setVisibility(View.VISIBLE); - skipButton.setVisibility(View.VISIBLE); } else { if (triesRemaining.getCount() == 1) { helpButton.setVisibility(View.VISIBLE); @@ -174,7 +173,6 @@ public class PinRestoreEntryFragment extends LoggingFragment { cancelSpinning(pinButton); pinEntry.setEnabled(true); enableAndFocusPinEntry(); - skipButton.setVisibility(View.VISIBLE); break; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java index a5bc26b245..7f055b5511 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java @@ -9,6 +9,7 @@ import androidx.annotation.WorkerThread; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.JobTracker; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; +import org.thoughtcrime.securesms.jobs.StorageForcePushJob; import org.thoughtcrime.securesms.keyvalue.KbsValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.lock.PinHashing; @@ -17,7 +18,9 @@ import org.thoughtcrime.securesms.lock.v2.PinKeyboardType; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.megaphone.Megaphones; import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException; +import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.KbsPinData; import org.whispersystems.signalservice.api.KeyBackupService; @@ -27,7 +30,6 @@ import org.whispersystems.signalservice.api.kbs.HashedPin; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; -import org.whispersystems.signalservice.internal.storage.protos.SignalStorage; import java.io.IOException; import java.util.Arrays; @@ -124,6 +126,8 @@ public final class PinState { * Invoked when the user is going through the PIN restoration flow (which is separate from reglock). */ public static synchronized void onSignalPinRestore(@NonNull Context context, @NonNull KbsPinData kbsData, @NonNull String pin) { + Log.i(TAG, "onSignalPinRestore()"); + SignalStore.kbsValues().setKbsMasterKey(kbsData, pin); SignalStore.kbsValues().setV2RegistrationLockEnabled(false); SignalStore.pinValues().resetPinReminders(); @@ -152,19 +156,10 @@ public final class PinState { { Log.i(TAG, "onPinChangedOrCreated()"); - KbsValues kbsValues = SignalStore.kbsValues(); - boolean isFirstPin = !kbsValues.hasPin(); - MasterKey masterKey = kbsValues.getOrCreateMasterKey(); - KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); - KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); - HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession); - KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey); + boolean isFirstPin = !SignalStore.kbsValues().hasPin() || SignalStore.kbsValues().hasOptedOut(); - kbsValues.setKbsMasterKey(kbsData, pin); - TextSecurePreferences.clearRegistrationLockV1(context); - SignalStore.pinValues().setKeyboardType(keyboard); - SignalStore.pinValues().resetPinReminders(); - ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL); + setPin(context, pin, keyboard); + SignalStore.kbsValues().optIn(); if (isFirstPin) { Log.i(TAG, "First time setting a PIN. Refreshing attributes to set the 'storage' capability."); @@ -180,11 +175,42 @@ public final class PinState { * Invoked when PIN creation fails. */ public static synchronized void onPinCreateFailure() { + Log.i(TAG, "onPinCreateFailure()"); if (getState() == State.NO_REGISTRATION_LOCK) { SignalStore.kbsValues().onPinCreateFailure(); } } + /** + * Invoked when the user has enabled the "PIN opt out" setting. + */ + @WorkerThread + public static synchronized void onPinOptOut(@NonNull Context context) + throws IOException, UnauthenticatedResponseException + { + Log.i(TAG, "onPinOptOutEnabled()"); + assertState(State.PIN_WITH_REGISTRATION_LOCK_DISABLED, State.NO_REGISTRATION_LOCK); + + optOutOfPin(context); + + updateState(buildInferredStateFromOtherFields()); + } + + /** + * Invoked when the user has chosen to skip PIN creation. + */ + @WorkerThread + public static synchronized void onPinCreationSkipped(@NonNull Context context) + throws IOException, UnauthenticatedResponseException + { + Log.i(TAG, "onPinCreationSkipped()"); + assertState(State.NO_REGISTRATION_LOCK); + + optOutOfPin(context); + + updateState(buildInferredStateFromOtherFields()); + } + /** * Invoked whenever a Signal PIN user enables registration lock. */ @@ -231,53 +257,6 @@ public final class PinState { updateState(State.PIN_WITH_REGISTRATION_LOCK_DISABLED); } - /** - * Called when registration lock is disabled in the settings using the old UI (i.e. no mention of - * Signal PINs). - */ - @WorkerThread - public static synchronized void onDisableLegacyRegistrationLockPreference(@NonNull Context context) - throws IOException, UnauthenticatedResponseException - { - Log.i(TAG, "onDisableRegistrationLockV1()"); - assertState(State.REGISTRATION_LOCK_V1); - - Log.i(TAG, "Removing v1 registration lock pin from server"); - ApplicationDependencies.getSignalServiceAccountManager().removeRegistrationLockV1(); - TextSecurePreferences.clearRegistrationLockV1(context); - - updateState(State.NO_REGISTRATION_LOCK); - } - - /** - * Called when registration lock is enabled in the settings using the old UI (i.e. no mention of - * Signal PINs). - */ - @WorkerThread - public static synchronized void onEnableLegacyRegistrationLockPreference(@NonNull Context context, @NonNull String pin) - throws IOException, UnauthenticatedResponseException - { - Log.i(TAG, "onCompleteRegistrationLockV1Reminder()"); - assertState(State.NO_REGISTRATION_LOCK); - - KbsValues kbsValues = SignalStore.kbsValues(); - MasterKey masterKey = kbsValues.getOrCreateMasterKey(); - KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); - KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); - HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession); - KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey); - - pinChangeSession.enableRegistrationLock(masterKey); - - kbsValues.setKbsMasterKey(kbsData, pin); - kbsValues.setV2RegistrationLockEnabled(true); - TextSecurePreferences.clearRegistrationLockV1(context); - TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); - TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL); - - updateState(buildInferredStateFromOtherFields()); - } - /** * Should only be called by {@link org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob}. */ @@ -285,6 +264,8 @@ public final class PinState { public static synchronized void onMigrateToRegistrationLockV2(@NonNull Context context, @NonNull String pin) throws IOException, UnauthenticatedResponseException { + Log.i(TAG, "onMigrateToRegistrationLockV2()"); + KbsValues kbsValues = SignalStore.kbsValues(); MasterKey masterKey = kbsValues.getOrCreateMasterKey(); KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); @@ -300,16 +281,12 @@ public final class PinState { updateState(buildInferredStateFromOtherFields()); } - public static synchronized boolean shouldShowRegistrationLockV1Reminder() { - return getState() == State.REGISTRATION_LOCK_V1; - } - @WorkerThread private static void bestEffortRefreshAttributes() { Optional result = ApplicationDependencies.getJobManager().runSynchronously(new RefreshAttributesJob(), TimeUnit.SECONDS.toMillis(10)); if (result.isPresent() && result.get() == JobTracker.JobState.SUCCESS) { - Log.w(TAG, "Attributes were refreshed successfully."); + Log.i(TAG, "Attributes were refreshed successfully."); } else if (result.isPresent()) { Log.w(TAG, "Attribute refresh finished, but was not successful. Enqueuing one for later. (Result: " + result.get() + ")"); ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()); @@ -344,6 +321,37 @@ public final class PinState { } } + @WorkerThread + private static void setPin(@NonNull Context context, @NonNull String pin, @NonNull PinKeyboardType keyboard) + throws IOException, UnauthenticatedResponseException + { + KbsValues kbsValues = SignalStore.kbsValues(); + MasterKey masterKey = kbsValues.getOrCreateMasterKey(); + KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(); + KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(); + HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession); + KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey); + + kbsValues.setKbsMasterKey(kbsData, pin); + TextSecurePreferences.clearRegistrationLockV1(context); + SignalStore.pinValues().setKeyboardType(keyboard); + SignalStore.pinValues().resetPinReminders(); + ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL); + } + + @WorkerThread + private static void optOutOfPin(@NonNull Context context) + throws IOException, UnauthenticatedResponseException + { + SignalStore.kbsValues().resetMasterKey(); + + setPin(context, Hex.toStringCondensed(Util.getSecretBytes(32)), PinKeyboardType.ALPHA_NUMERIC); + SignalStore.kbsValues().optOut(); + + ApplicationDependencies.getJobManager().add(new StorageForcePushJob()); + bestEffortRefreshAttributes(); + } + private static @NonNull State assertState(State... allowed) { State currentState = getState(); @@ -358,6 +366,7 @@ public final class PinState { case REGISTRATION_LOCK_V1: throw new InvalidState_RegistrationLockV1(); case PIN_WITH_REGISTRATION_LOCK_ENABLED: throw new InvalidState_PinWithRegistrationLockEnabled(); case PIN_WITH_REGISTRATION_LOCK_DISABLED: throw new InvalidState_PinWithRegistrationLockDisabled(); + case PIN_OPT_OUT: throw new InvalidState_PinOptOut(); default: throw new IllegalStateException("Expected: " + Arrays.toString(allowed) + ", Actual: " + currentState); } } @@ -386,6 +395,11 @@ public final class PinState { boolean v1Enabled = TextSecurePreferences.isV1RegistrationLockEnabled(context); boolean v2Enabled = kbsValues.isV2RegistrationLockEnabled(); boolean hasPin = kbsValues.hasPin(); + boolean optedOut = kbsValues.hasOptedOut(); + + if (optedOut && !v2Enabled && !v1Enabled) { + return State.PIN_OPT_OUT; + } if (!v1Enabled && !v2Enabled && !hasPin) { return State.NO_REGISTRATION_LOCK; @@ -427,7 +441,13 @@ public final class PinState { /** * User has a PIN, but registration lock is disabled. */ - PIN_WITH_REGISTRATION_LOCK_DISABLED("pin_with_registration_lock_disabled"); + PIN_WITH_REGISTRATION_LOCK_DISABLED("pin_with_registration_lock_disabled"), + + /** + * The user has opted out of creating a PIN. In this case, we will generate a high-entropy PIN + * on their behalf. + */ + PIN_OPT_OUT("pin_opt_out"); /** * Using a string key so that people can rename/reorder values in the future without breaking @@ -463,4 +483,5 @@ public final class PinState { private static class InvalidState_RegistrationLockV1 extends IllegalStateException {} private static class InvalidState_PinWithRegistrationLockEnabled extends IllegalStateException {} private static class InvalidState_PinWithRegistrationLockDisabled extends IllegalStateException {} + private static class InvalidState_PinOptOut extends IllegalStateException {} } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinPreferenceFragment.java new file mode 100644 index 0000000000..3ca195cd99 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPinPreferenceFragment.java @@ -0,0 +1,95 @@ +package org.thoughtcrime.securesms.preferences; + +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; + +import com.google.android.material.snackbar.Snackbar; + +import org.thoughtcrime.securesms.ApplicationPreferencesActivity; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; +import org.thoughtcrime.securesms.pin.PinOptOutDialog; +import org.thoughtcrime.securesms.util.TextSecurePreferences; + +public class AdvancedPinPreferenceFragment extends ListSummaryPreferenceFragment { + + private static final String PREF_ENABLE = "pref_pin_enable"; + private static final String PREF_DISABLE = "pref_pin_disable"; + + @Override + public void onCreate(Bundle paramBundle) { + super.onCreate(paramBundle); + } + + @Override + public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + addPreferencesFromResource(R.xml.preferences_advanced_pin); + } + + @Override + public void onResume() { + super.onResume(); + updatePreferenceState(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == CreateKbsPinActivity.REQUEST_NEW_PIN && resultCode == CreateKbsPinActivity.RESULT_OK) { + Snackbar.make(requireView(), R.string.ApplicationPreferencesActivity_pin_created, Snackbar.LENGTH_LONG).setTextColor(Color.WHITE).show(); + } + } + + private void updatePreferenceState() { + Preference enable = this.findPreference(PREF_ENABLE); + Preference disable = this.findPreference(PREF_DISABLE); + + if (SignalStore.kbsValues().hasOptedOut()) { + enable.setVisible(true); + disable.setVisible(false); + + enable.setOnPreferenceClickListener(preference -> { + onPreferenceChanged(true); + return true; + }); + } else { + enable.setVisible(false); + disable.setVisible(true); + + disable.setOnPreferenceClickListener(preference -> { + onPreferenceChanged(false); + return true; + }); + } + + ((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.preferences__advanced_pin_settings); + } + + private void onPreferenceChanged(boolean enabled) { + boolean hasRegistrationLock = TextSecurePreferences.isV1RegistrationLockEnabled(requireContext()) || + SignalStore.kbsValues().isV2RegistrationLockEnabled(); + + if (!enabled && hasRegistrationLock) { + new AlertDialog.Builder(requireContext()) + .setMessage(R.string.ApplicationPreferencesActivity_pins_are_required_for_registration_lock) + .setCancelable(true) + .setPositiveButton(android.R.string.ok, (d, which) -> d.dismiss()) + .show(); + } else if (!enabled) { + PinOptOutDialog.showForOptOut(requireContext(), + () -> { + updatePreferenceState(); + Snackbar.make(requireView(), R.string.ApplicationPreferencesActivity_pin_disabled, Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show(); + }, + () -> Toast.makeText(requireContext(), R.string.ApplicationPreferencesActivity_failed_to_disable_pins_try_again_later, Toast.LENGTH_LONG).show()); + } else { + startActivityForResult(CreateKbsPinActivity.getIntentForPinCreate(requireContext()), CreateKbsPinActivity.REQUEST_NEW_PIN); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java index 1093a45d22..62b7d1ee10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AdvancedPreferenceFragment.java @@ -24,16 +24,23 @@ import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactIdentityManager; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.keyvalue.KbsValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity; +import org.thoughtcrime.securesms.pin.PinOptOutDialog; +import org.thoughtcrime.securesms.pin.PinState; import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; +import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import java.io.IOException; @@ -43,6 +50,7 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment { private static final String PUSH_MESSAGING_PREF = "pref_toggle_push_messaging"; private static final String SUBMIT_DEBUG_LOG_PREF = "pref_submit_debug_logs"; private static final String INTERNAL_PREF = "pref_internal"; + private static final String ADVANCED_PIN_PREF = "pref_advanced_pin_settings"; private static final int PICK_IDENTITY_CONTACT = 1; @@ -56,6 +64,17 @@ public class AdvancedPreferenceFragment extends CorrectedPreferenceFragment { submitDebugLog.setOnPreferenceClickListener(new SubmitDebugLogListener()); submitDebugLog.setSummary(getVersion(getActivity())); + Preference pinSettings = this.findPreference(ADVANCED_PIN_PREF); + pinSettings.setOnPreferenceClickListener(preference -> { + requireActivity().getSupportFragmentManager() + .beginTransaction() + .setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end) + .replace(android.R.id.content, new AdvancedPinPreferenceFragment()) + .addToBackStack(null) + .commit(); + return false; + }); + Preference internalPreference = this.findPreference(INTERNAL_PREF); internalPreference.setVisible(FeatureFlags.internalUser()); internalPreference.setOnPreferenceClickListener(preference -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index 37db2b0719..6bb68c486e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -119,9 +119,10 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment SwitchPreferenceCompat signalPinReminders = (SwitchPreferenceCompat) this.findPreference(PinValues.PIN_REMINDERS_ENABLED); SwitchPreferenceCompat registrationLockV2 = (SwitchPreferenceCompat) this.findPreference(KbsValues.V2_LOCK_ENABLED); - if (SignalStore.kbsValues().hasPin()) { + if (SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().hasOptedOut()) { signalPinCreateChange.setOnPreferenceClickListener(new KbsPinUpdateListener()); signalPinCreateChange.setTitle(R.string.preferences_app_protection__change_your_pin); + signalPinReminders.setEnabled(true); registrationLockV2.setEnabled(true); } else { signalPinCreateChange.setOnPreferenceClickListener(new KbsPinCreateListener()); diff --git a/app/src/main/res/layout/base_kbs_pin_fragment.xml b/app/src/main/res/layout/base_kbs_pin_fragment.xml index 87e0e7cdf4..0f07170960 100644 --- a/app/src/main/res/layout/base_kbs_pin_fragment.xml +++ b/app/src/main/res/layout/base_kbs_pin_fragment.xml @@ -11,6 +11,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index f171b379e2..85855f14eb 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -226,6 +226,8 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 20a0d49506..b1440a7c8b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,6 +53,10 @@ Screen lock %1$s, Registration lock %2$s Screen lock %1$s Theme %1$s, Language %2$s + PINs are required for registration lock. To disable PINs, please first disable registration lock. + Failed to disable PINs. Try again later. + Pin created. + Pin disabled. @@ -953,6 +957,14 @@ Create new PIN https://support.signal.org/hc/articles/360007059792 + + Warning + Skipping PIN creation will create a hidden, high-entropy PIN associated with your account that is unrecoverable. When you re-register Signal you will lose all data unless you manually back up and restore. You can not turn on Registration Lock while the PIN is disabled. + Skip PIN creation + Disable PIN? + Disabling PIN will create a hidden, high-entropy PIN associated with your account that is unrecoverable. When you re-register Signal you will lose all data unless you manually back up and restore. You can not turn on Registration Lock while the PIN is disabled. + Disable PIN + Rate this app If you enjoy using this app, please take a moment to help us by rating it. @@ -1892,10 +1904,15 @@ Dark Appearance Theme + Disable PIN + Enable PIN + Disabling PIN will create a hidden, high-entropy PIN associated with your account that is unrecoverable. When you re-register Signal you will lose all data unless you manually back up and restore. You can not turn on Registration Lock while the PIN is disabled. + PINs keep information stored with Signal encrypted so only you can access it. Your profile, settings, and contacts will restore when you reinstall. You won’t need your PIN to open the app. System default Default Language Signal messages and calls + Advanced PIN settings Free private messages and calls to Signal users Submit debug log \'WiFi Calling\' compatibility mode @@ -2151,7 +2168,7 @@ Create a new PIN You can change your PIN as long as this device is registered. Create your PIN - PINs keep information stored with Signal encrypted so only you can access it. Your profile, settings, and contacts will restore when you reinstall Signal. + PINs keep information stored with Signal encrypted so only you can access it. Your profile, settings, and contacts will restore when you reinstall. You won\'t need your PIN to open the app. Choose a stronger PIN @@ -2165,7 +2182,7 @@ Introducing PINs - PINs keep information stored with Signal encrypted so only you can access it. Your profile, settings, and contacts will restore when you reinstall Signal. + PINs keep information stored with Signal encrypted so only you can access it. Your profile, settings, and contacts will restore when you reinstall. You won\'t need your PIN to open the app. Learn More https://support.signal.org/hc/articles/360007059792 Registration Lock = PIN @@ -2173,6 +2190,8 @@ Read more about PINs. Update PIN Create your PIN + Learn more about PINs + Skip PIN creation Enter your Signal PIN diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 3a9e495c1d..72ae22e0ca 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -410,6 +410,8 @@ @color/core_white + @color/core_red_highlight + ?attr/icon_tint @drawable/preference_divider_light @@ -710,6 +712,8 @@ @color/core_grey_75 + @color/core_red + ?attr/icon_tint @drawable/preference_divider_dark diff --git a/app/src/main/res/xml/preferences_advanced.xml b/app/src/main/res/xml/preferences_advanced.xml index b6d70eb33f..ee80c165a2 100644 --- a/app/src/main/res/xml/preferences_advanced.xml +++ b/app/src/main/res/xml/preferences_advanced.xml @@ -1,22 +1,30 @@ - + + android:defaultValue="false" + android:key="pref_toggle_push_messaging" + android:title="@string/preferences__signal_messages_and_calls" + android:summary="@string/preferences__free_private_messages_and_calls"/> - + - + + android:title="@string/preferences__internal_preferences" + app:isPreferenceVisible="false" /> + + diff --git a/app/src/main/res/xml/preferences_advanced_pin.xml b/app/src/main/res/xml/preferences_advanced_pin.xml new file mode 100644 index 0000000000..74fc63781e --- /dev/null +++ b/app/src/main/res/xml/preferences_advanced_pin.xml @@ -0,0 +1,22 @@ + + + + + + + + +