mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 08:09:12 +01:00
Update registration to allow PIN entry.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package org.thoughtcrime.securesms.pin;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
|
||||
|
||||
public final class PinRestoreActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.pin_restore_activity);
|
||||
}
|
||||
|
||||
void navigateToPinCreation() {
|
||||
final Intent main = new Intent(this, MainActivity.class);
|
||||
final Intent createPin = CreateKbsPinActivity.getIntentForPinCreate(this);
|
||||
final Intent chained = PassphraseRequiredActionBarActivity.chainIntent(createPin, main);
|
||||
|
||||
startActivity(chained);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package org.thoughtcrime.securesms.pin;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputType;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.navigation.Navigation;
|
||||
|
||||
import com.dd.CircularProgressButton;
|
||||
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class PinRestoreEntryFragment extends Fragment {
|
||||
private static final String TAG = Log.tag(PinRestoreActivity.class);
|
||||
|
||||
private static final int MINIMUM_PIN_LENGTH = 4;
|
||||
|
||||
private EditText pinEntry;
|
||||
private View helpButton;
|
||||
private View skipButton;
|
||||
private CircularProgressButton pinButton;
|
||||
private TextView errorLabel;
|
||||
private TextView keyboardToggle;
|
||||
private PinRestoreViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.pin_restore_entry_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
initViews(view);
|
||||
initViewModel();
|
||||
}
|
||||
|
||||
private void initViews(@NonNull View root) {
|
||||
pinEntry = root.findViewById(R.id.pin_restore_pin_input);
|
||||
pinButton = root.findViewById(R.id.pin_restore_pin_confirm);
|
||||
errorLabel = root.findViewById(R.id.pin_restore_pin_input_label);
|
||||
keyboardToggle = root.findViewById(R.id.pin_restore_keyboard_toggle);
|
||||
helpButton = root.findViewById(R.id.pin_restore_forgot_pin);
|
||||
skipButton = root.findViewById(R.id.pin_restore_skip_button);
|
||||
|
||||
helpButton.setVisibility(View.GONE);
|
||||
helpButton.setOnClickListener(v -> onNeedHelpClicked());
|
||||
|
||||
skipButton.setOnClickListener(v -> onSkipClicked());
|
||||
|
||||
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
ViewUtil.hideKeyboard(requireContext(), v);
|
||||
onPinSubmitted();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
enableAndFocusPinEntry();
|
||||
|
||||
pinButton.setOnClickListener((v) -> {
|
||||
ViewUtil.hideKeyboard(requireContext(), pinEntry);
|
||||
onPinSubmitted();
|
||||
});
|
||||
|
||||
keyboardToggle.setOnClickListener((v) -> {
|
||||
PinKeyboardType keyboardType = getPinEntryKeyboardType();
|
||||
|
||||
updateKeyboard(keyboardType.getOther());
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
});
|
||||
|
||||
PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther();
|
||||
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
|
||||
}
|
||||
|
||||
private void initViewModel() {
|
||||
viewModel = ViewModelProviders.of(this).get(PinRestoreViewModel.class);
|
||||
|
||||
viewModel.getTriesRemaining().observe(this, this::presentTriesRemaining);
|
||||
viewModel.getEvent().observe(this, this::presentEvent);
|
||||
}
|
||||
|
||||
private void presentTriesRemaining(PinRestoreViewModel.TriesRemaining triesRemaining) {
|
||||
if (triesRemaining.hasIncorrectGuess()) {
|
||||
if (triesRemaining.getCount() == 1) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_incorrect_pin)
|
||||
.setMessage(getResources().getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining.getCount(), triesRemaining.getCount()))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
errorLabel.setText(R.string.PinRestoreEntryFragment_incorrect_pin);
|
||||
helpButton.setVisibility(View.VISIBLE);
|
||||
skipButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
if (triesRemaining.getCount() == 1) {
|
||||
helpButton.setVisibility(View.VISIBLE);
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setMessage(getResources().getQuantityString(R.plurals.PinRestoreEntryFragment_you_have_d_attempt_remaining, triesRemaining.getCount(), triesRemaining.getCount()))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
if (triesRemaining.getCount() == 0) {
|
||||
Log.w(TAG, "Account locked. User out of attempts on KBS.");
|
||||
onAccountLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private void presentEvent(@NonNull PinRestoreViewModel.Event event) {
|
||||
switch (event) {
|
||||
case SUCCESS:
|
||||
handleSuccess();
|
||||
break;
|
||||
case EMPTY_PIN:
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show();
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.getText().clear();
|
||||
enableAndFocusPinEntry();
|
||||
break;
|
||||
case PIN_TOO_SHORT:
|
||||
Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show();
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.getText().clear();
|
||||
enableAndFocusPinEntry();
|
||||
break;
|
||||
case PIN_INCORRECT:
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.getText().clear();
|
||||
enableAndFocusPinEntry();
|
||||
break;
|
||||
case PIN_LOCKED:
|
||||
onAccountLocked();
|
||||
break;
|
||||
case NETWORK_ERROR:
|
||||
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
|
||||
cancelSpinning(pinButton);
|
||||
pinEntry.setEnabled(true);
|
||||
enableAndFocusPinEntry();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private PinKeyboardType getPinEntryKeyboardType() {
|
||||
boolean isNumeric = (pinEntry.getInputType() & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_NUMBER;
|
||||
|
||||
return isNumeric ? PinKeyboardType.NUMERIC : PinKeyboardType.ALPHA_NUMERIC;
|
||||
}
|
||||
|
||||
private void onPinSubmitted() {
|
||||
pinEntry.setEnabled(false);
|
||||
viewModel.onPinSubmitted(pinEntry.getText().toString(), getPinEntryKeyboardType());
|
||||
setSpinning(pinButton);
|
||||
}
|
||||
|
||||
private void onNeedHelpClicked() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_need_help)
|
||||
.setMessage(getString(R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code, KbsConstants.MINIMUM_PIN_LENGTH))
|
||||
.setPositiveButton(R.string.PinRestoreEntryFragment_create_new_pin, null)
|
||||
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> {
|
||||
CommunicationActions.openEmail(requireContext(),
|
||||
getString(R.string.PinRestoreEntryFragment_support_email),
|
||||
getString(R.string.PinRestoreEntryFragment_signal_registration_need_help_with_pin),
|
||||
getString(R.string.PinRestoreEntryFragment_subject_signal_registration,
|
||||
getDevice(),
|
||||
getAndroidVersion(),
|
||||
BuildConfig.VERSION_NAME,
|
||||
Locale.getDefault()));
|
||||
})
|
||||
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onSkipClicked() {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.PinRestoreEntryFragment_skip_pin_entry)
|
||||
.setMessage(R.string.PinRestoreEntryFragment_if_you_cant_remember_your_pin)
|
||||
.setPositiveButton(R.string.PinRestoreEntryFragment_create_new_pin, (dialog, which) -> {
|
||||
PinState.onPinRestoreForgottenOrSkipped();
|
||||
((PinRestoreActivity) requireActivity()).navigateToPinCreation();
|
||||
})
|
||||
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onAccountLocked() {
|
||||
Navigation.findNavController(requireView()).navigate(PinRestoreEntryFragmentDirections.actionAccountLocked());
|
||||
}
|
||||
|
||||
private void handleSuccess() {
|
||||
cancelSpinning(pinButton);
|
||||
|
||||
Activity activity = requireActivity();
|
||||
|
||||
if (Recipient.self().getProfileName().isEmpty() || !AvatarHelper.hasAvatar(activity, Recipient.self().getId())) {
|
||||
final Intent main = new Intent(activity, MainActivity.class);
|
||||
final Intent profile = EditProfileActivity.getIntent(activity, false);
|
||||
|
||||
profile.putExtra("next_intent", main);
|
||||
startActivity(profile);
|
||||
} else {
|
||||
startActivity(new Intent(activity, MainActivity.class));
|
||||
}
|
||||
|
||||
activity.finish();
|
||||
}
|
||||
|
||||
private void updateKeyboard(@NonNull PinKeyboardType keyboard) {
|
||||
boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC;
|
||||
|
||||
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
|
||||
|
||||
pinEntry.getText().clear();
|
||||
}
|
||||
|
||||
private @StringRes static int resolveKeyboardToggleText(@NonNull PinKeyboardType keyboard) {
|
||||
if (keyboard == PinKeyboardType.ALPHA_NUMERIC) {
|
||||
return R.string.PinRestoreEntryFragment_enter_alphanumeric_pin;
|
||||
} else {
|
||||
return R.string.PinRestoreEntryFragment_enter_numeric_pin;
|
||||
}
|
||||
}
|
||||
|
||||
private void enableAndFocusPinEntry() {
|
||||
pinEntry.setEnabled(true);
|
||||
pinEntry.setFocusable(true);
|
||||
|
||||
if (pinEntry.requestFocus()) {
|
||||
ServiceUtil.getInputMethodManager(pinEntry.getContext()).showSoftInput(pinEntry, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setSpinning(@Nullable CircularProgressButton button) {
|
||||
if (button != null) {
|
||||
button.setClickable(false);
|
||||
button.setIndeterminateProgressMode(true);
|
||||
button.setProgress(50);
|
||||
}
|
||||
}
|
||||
|
||||
private static void cancelSpinning(@Nullable CircularProgressButton button) {
|
||||
if (button != null) {
|
||||
button.setProgress(0);
|
||||
button.setIndeterminateProgressMode(false);
|
||||
button.setClickable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getDevice() {
|
||||
return String.format("%s %s (%s)", Build.MANUFACTURER, Build.MODEL, Build.PRODUCT);
|
||||
}
|
||||
|
||||
private static String getAndroidVersion() {
|
||||
return String.format("%s (%s, %s)", Build.VERSION.RELEASE, Build.VERSION.INCREMENTAL, Build.DISPLAY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.thoughtcrime.securesms.pin;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
|
||||
public class PinRestoreLockedFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.pin_restore_locked_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
View createPinButton = view.findViewById(R.id.pin_locked_next);
|
||||
View learnMoreButton = view.findViewById(R.id.pin_locked_learn_more);
|
||||
|
||||
createPinButton.setOnClickListener(v -> {
|
||||
PinState.onPinRestoreForgottenOrSkipped();
|
||||
((PinRestoreActivity) requireActivity()).navigateToPinCreation();
|
||||
});
|
||||
|
||||
learnMoreButton.setOnClickListener(v -> {
|
||||
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.PinRestoreLockedFragment_learn_more_url));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.thoughtcrime.securesms.pin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.KbsPinData;
|
||||
import org.whispersystems.signalservice.api.KeyBackupService;
|
||||
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
class PinRestoreRepository {
|
||||
|
||||
private static final String TAG = Log.tag(PinRestoreRepository.class);
|
||||
|
||||
private final Executor executor = SignalExecutors.UNBOUNDED;
|
||||
private final KeyBackupService kbs = ApplicationDependencies.getKeyBackupService();
|
||||
|
||||
void getToken(@NonNull Callback<Optional<TokenData>> callback) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
String authorization = kbs.getAuthorization();
|
||||
TokenResponse token = kbs.getToken(authorization);
|
||||
TokenData tokenData = new TokenData(authorization, token);
|
||||
callback.onComplete(Optional.of(tokenData));
|
||||
} catch (IOException e) {
|
||||
callback.onComplete(Optional.absent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void submitPin(@NonNull String pin, @NonNull TokenData tokenData, @NonNull Callback<PinResultData> callback) {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
Stopwatch stopwatch = new Stopwatch("PinSubmission");
|
||||
|
||||
KbsPinData kbsData = PinState.restoreMasterKey(pin, tokenData.basicAuth, tokenData.tokenResponse);
|
||||
PinState.onSignalPinRestore(ApplicationDependencies.getApplication(), Objects.requireNonNull(kbsData), pin);
|
||||
stopwatch.split("MasterKey");
|
||||
|
||||
ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
|
||||
stopwatch.split("AccountRestore");
|
||||
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
callback.onComplete(new PinResultData(PinResult.SUCCESS, tokenData));
|
||||
} catch (IOException e) {
|
||||
callback.onComplete(new PinResultData(PinResult.NETWORK_ERROR, tokenData));
|
||||
} catch (KeyBackupSystemNoDataException e) {
|
||||
callback.onComplete(new PinResultData(PinResult.LOCKED, tokenData));
|
||||
} catch (KeyBackupSystemWrongPinException e) {
|
||||
callback.onComplete(new PinResultData(PinResult.INCORRECT, new TokenData(tokenData.basicAuth, e.getTokenResponse())));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
interface Callback<T> {
|
||||
void onComplete(@NonNull T value);
|
||||
}
|
||||
|
||||
static class TokenData {
|
||||
private final String basicAuth;
|
||||
private final TokenResponse tokenResponse;
|
||||
|
||||
TokenData(@NonNull String basicAuth, @NonNull TokenResponse tokenResponse) {
|
||||
this.basicAuth = basicAuth;
|
||||
this.tokenResponse = tokenResponse;
|
||||
}
|
||||
|
||||
int getTriesRemaining() {
|
||||
return tokenResponse.getTries();
|
||||
}
|
||||
}
|
||||
|
||||
static class PinResultData {
|
||||
private final PinResult result;
|
||||
private final TokenData tokenData;
|
||||
|
||||
PinResultData(@NonNull PinResult result, @NonNull TokenData tokenData) {
|
||||
this.result = result;
|
||||
this.tokenData = tokenData;
|
||||
}
|
||||
|
||||
public @NonNull PinResult getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public @NonNull TokenData getTokenData() {
|
||||
return tokenData;
|
||||
}
|
||||
}
|
||||
|
||||
enum PinResult {
|
||||
SUCCESS, INCORRECT, LOCKED, NETWORK_ERROR
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.thoughtcrime.securesms.pin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
|
||||
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
|
||||
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
|
||||
public class PinRestoreViewModel extends ViewModel {
|
||||
|
||||
private final PinRestoreRepository repo;
|
||||
private final DefaultValueLiveData<TriesRemaining> triesRemaining;
|
||||
private final SingleLiveEvent<Event> event;
|
||||
|
||||
private volatile PinRestoreRepository.TokenData tokenData;
|
||||
|
||||
public PinRestoreViewModel() {
|
||||
this.repo = new PinRestoreRepository();
|
||||
this.triesRemaining = new DefaultValueLiveData<>(new TriesRemaining(10, false));
|
||||
this.event = new SingleLiveEvent<>();
|
||||
|
||||
repo.getToken(token -> {
|
||||
if (token.isPresent()) {
|
||||
updateTokenData(token.get(), false);
|
||||
} else {
|
||||
event.postValue(Event.NETWORK_ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onPinSubmitted(@NonNull String pin, @NonNull PinKeyboardType pinKeyboardType) {
|
||||
int trimmedLength = pin.replace(" ", "").length();
|
||||
|
||||
if (trimmedLength == 0) {
|
||||
event.postValue(Event.EMPTY_PIN);
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmedLength < KbsConstants.MINIMUM_PIN_LENGTH) {
|
||||
event.postValue(Event.PIN_TOO_SHORT);
|
||||
return;
|
||||
}
|
||||
|
||||
if (tokenData != null) {
|
||||
repo.submitPin(pin, tokenData, result -> {
|
||||
|
||||
switch (result.getResult()) {
|
||||
case SUCCESS:
|
||||
SignalStore.pinValues().setKeyboardType(pinKeyboardType);
|
||||
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
|
||||
event.postValue(Event.SUCCESS);
|
||||
break;
|
||||
case LOCKED:
|
||||
event.postValue(Event.PIN_LOCKED);
|
||||
break;
|
||||
case INCORRECT:
|
||||
event.postValue(Event.PIN_INCORRECT);
|
||||
updateTokenData(result.getTokenData(), true);
|
||||
break;
|
||||
case NETWORK_ERROR:
|
||||
event.postValue(Event.NETWORK_ERROR);
|
||||
break;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
repo.getToken(token -> {
|
||||
if (token.isPresent()) {
|
||||
updateTokenData(token.get(), false);
|
||||
onPinSubmitted(pin, pinKeyboardType);
|
||||
} else {
|
||||
event.postValue(Event.NETWORK_ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull DefaultValueLiveData<TriesRemaining> getTriesRemaining() {
|
||||
return triesRemaining;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Event> getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
private void updateTokenData(@NonNull PinRestoreRepository.TokenData tokenData, boolean incorrectGuess) {
|
||||
this.tokenData = tokenData;
|
||||
triesRemaining.postValue(new TriesRemaining(tokenData.getTriesRemaining(), incorrectGuess));
|
||||
}
|
||||
|
||||
enum Event {
|
||||
SUCCESS, EMPTY_PIN, PIN_TOO_SHORT, PIN_INCORRECT, PIN_LOCKED, NETWORK_ERROR
|
||||
}
|
||||
|
||||
static class TriesRemaining {
|
||||
private final int triesRemaining;
|
||||
private final boolean hasIncorrectGuess;
|
||||
|
||||
TriesRemaining(int triesRemaining, boolean hasIncorrectGuess) {
|
||||
this.triesRemaining = triesRemaining;
|
||||
this.hasIncorrectGuess = hasIncorrectGuess;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return triesRemaining;
|
||||
}
|
||||
|
||||
public boolean hasIncorrectGuess() {
|
||||
return hasIncorrectGuess;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import org.whispersystems.signalservice.internal.contacts.crypto.Unauthenticated
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -84,35 +85,63 @@ public final class PinState {
|
||||
*/
|
||||
public static synchronized void onRegistration(@NonNull Context context,
|
||||
@Nullable KbsPinData kbsData,
|
||||
@Nullable String pin)
|
||||
@Nullable String pin,
|
||||
boolean hasPinToRestore)
|
||||
{
|
||||
Log.i(TAG, "onNewRegistration()");
|
||||
|
||||
if (kbsData == null) {
|
||||
Log.i(TAG, "No KBS PIN. Clearing any PIN state.");
|
||||
SignalStore.kbsValues().clearRegistrationLockAndPin();
|
||||
//noinspection deprecation Only acceptable place to write the old pin.
|
||||
TextSecurePreferences.setV1RegistrationLockPin(context, pin);
|
||||
//noinspection deprecation Only acceptable place to write the old pin enabled state.
|
||||
TextSecurePreferences.setV1RegistrationLockEnabled(context, pin != null);
|
||||
} else {
|
||||
Log.i(TAG, "Had a KBS PIN. Saving data.");
|
||||
SignalStore.kbsValues().setKbsMasterKey(kbsData, PinHashing.localPinHash(pin));
|
||||
// TODO [greyson] [pins] Not always true -- when this flow is reworked, you can have a PIN but no reglock
|
||||
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
|
||||
resetPinRetryCount(context, pin, kbsData);
|
||||
}
|
||||
TextSecurePreferences.setV1RegistrationLockPin(context, pin);
|
||||
|
||||
if (pin != null) {
|
||||
if (kbsData == null && pin != null) {
|
||||
Log.i(TAG, "Registration Lock V1");
|
||||
SignalStore.kbsValues().clearRegistrationLockAndPin();
|
||||
TextSecurePreferences.setV1RegistrationLockEnabled(context, true);
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
|
||||
} else if (kbsData != null && pin != null) {
|
||||
Log.i(TAG, "Registration Lock V2");
|
||||
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
|
||||
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
|
||||
SignalStore.kbsValues().setKbsMasterKey(kbsData, PinHashing.localPinHash(pin));
|
||||
SignalStore.pinValues().resetPinReminders();
|
||||
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
|
||||
resetPinRetryCount(context, pin, kbsData);
|
||||
} else if (hasPinToRestore) {
|
||||
Log.i(TAG, "Has a PIN to restore.");
|
||||
SignalStore.kbsValues().clearRegistrationLockAndPin();
|
||||
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
|
||||
SignalStore.storageServiceValues().setNeedsAccountRestore(true);
|
||||
} else {
|
||||
Log.i(TAG, "No registration lock or PIN at all.");
|
||||
SignalStore.kbsValues().clearRegistrationLockAndPin();
|
||||
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
|
||||
}
|
||||
|
||||
updateState(buildInferredStateFromOtherFields());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
SignalStore.kbsValues().setKbsMasterKey(kbsData, PinHashing.localPinHash(pin));
|
||||
SignalStore.kbsValues().setV2RegistrationLockEnabled(false);
|
||||
SignalStore.pinValues().resetPinReminders();
|
||||
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
|
||||
resetPinRetryCount(context, pin, kbsData);
|
||||
|
||||
updateState(buildInferredStateFromOtherFields());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when the user skips out on PIN restoration or otherwise fails to remember their PIN.
|
||||
*/
|
||||
public static synchronized void onPinRestoreForgottenOrSkipped() {
|
||||
SignalStore.kbsValues().clearRegistrationLockAndPin();
|
||||
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
|
||||
|
||||
updateState(buildInferredStateFromOtherFields());
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked whenever the Signal PIN is changed or created.
|
||||
*/
|
||||
@@ -181,10 +210,11 @@ public final class PinState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked whenever registration lock is disabled for a user without a Signal PIN.
|
||||
* 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 onDisableRegistrationLockV1(@NonNull Context context)
|
||||
public static synchronized void onDisableLegacyRegistrationLockPreference(@NonNull Context context)
|
||||
throws IOException, UnauthenticatedResponseException
|
||||
{
|
||||
Log.i(TAG, "onDisableRegistrationLockV1()");
|
||||
@@ -197,12 +227,16 @@ public final class PinState {
|
||||
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 onCompleteRegistrationLockV1Reminder(@NonNull Context context, @NonNull String pin)
|
||||
public static synchronized void onEnableLegacyRegistrationLockPreference(@NonNull Context context, @NonNull String pin)
|
||||
throws IOException, UnauthenticatedResponseException
|
||||
{
|
||||
Log.i(TAG, "onCompleteRegistrationLockV1Reminder()");
|
||||
assertState(State.REGISTRATION_LOCK_V1);
|
||||
assertState(State.NO_REGISTRATION_LOCK);
|
||||
|
||||
KbsValues kbsValues = SignalStore.kbsValues();
|
||||
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
|
||||
@@ -219,7 +253,29 @@ public final class PinState {
|
||||
TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis());
|
||||
TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL);
|
||||
|
||||
updateState(State.PIN_WITH_REGISTRATION_LOCK_ENABLED);
|
||||
updateState(buildInferredStateFromOtherFields());
|
||||
}
|
||||
|
||||
/**
|
||||
* Should only be called by {@link org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob}.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static synchronized void onMigrateToRegistrationLockV2(@NonNull Context context, @NonNull String pin)
|
||||
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);
|
||||
|
||||
pinChangeSession.enableRegistrationLock(masterKey);
|
||||
|
||||
kbsValues.setKbsMasterKey(kbsData, PinHashing.localPinHash(pin));
|
||||
TextSecurePreferences.clearRegistrationLockV1(context);
|
||||
|
||||
updateState(buildInferredStateFromOtherFields());
|
||||
}
|
||||
|
||||
public static synchronized boolean shouldShowRegistrationLockV1Reminder() {
|
||||
@@ -273,7 +329,7 @@ public final class PinState {
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException();
|
||||
throw new IllegalStateException("Expected: " + Arrays.toString(allowed) + ", Actual: " + currentState);
|
||||
}
|
||||
|
||||
private static @NonNull State getState() {
|
||||
@@ -289,6 +345,7 @@ public final class PinState {
|
||||
}
|
||||
|
||||
private static void updateState(@NonNull State state) {
|
||||
Log.i(TAG, "Updating state to: " + state);
|
||||
SignalStore.pinValues().setPinState(state.serialize());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user