Start mirroring to SVR2.

This commit is contained in:
Greyson Parrelli
2023-07-05 19:05:30 -04:00
committed by Clark Chen
parent dfb7304626
commit e1570e9512
111 changed files with 1828 additions and 2299 deletions

View File

@@ -1,183 +0,0 @@
package org.thoughtcrime.securesms.pin;
import android.app.backup.BackupManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.svr2.PinHash;
import org.thoughtcrime.securesms.KbsEnclave;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.kbs.PinHashUtil;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
/**
* Using provided or already stored authorization, provides various get token data from KBS
* and generate {@link KbsPinData}.
*/
public class KbsRepository {
private static final String TAG = Log.tag(KbsRepository.class);
public void getToken(@NonNull Consumer<Optional<TokenData>> callback) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {
callback.accept(Optional.ofNullable(getTokenSync(null)));
} catch (IOException e) {
callback.accept(Optional.empty());
}
});
}
/**
* @param authorization If this is being called before the user is registered (i.e. as part of
* reglock), you must pass in an authorization token that can be used to
* retrieve a backup. Otherwise, pass in null and we'll fetch one.
*/
public Single<ServiceResponse<TokenData>> getToken(@Nullable String authorization) {
return Single.<ServiceResponse<TokenData>>fromCallable(() -> {
try {
return ServiceResponse.forResult(getTokenSync(authorization), 200, null);
} catch (IOException e) {
return ServiceResponse.forUnknownError(e);
}
}).subscribeOn(Schedulers.io());
}
/**
* Fetch and store a new KBS authorization.
*/
public void refreshAuthorization() throws IOException {
for (KbsEnclave enclave : KbsEnclaves.all()) {
KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave);
try {
String authorization = kbs.getAuthorization();
backupAuthToken(authorization);
} catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 404) {
Log.i(TAG, "Enclave decommissioned, skipping", e);
} else {
throw e;
}
}
}
}
private @NonNull TokenData getTokenSync(@Nullable String authorization) throws IOException {
TokenData firstKnownTokenData = null;
for (KbsEnclave enclave : KbsEnclaves.all()) {
KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave);
TokenResponse token;
try {
authorization = authorization == null ? kbs.getAuthorization() : authorization;
backupAuthToken(authorization);
token = kbs.getToken(authorization);
} catch (NonSuccessfulResponseCodeException e) {
if (e.getCode() == 404) {
Log.i(TAG, "Enclave decommissioned, skipping", e);
continue;
} else {
throw e;
}
}
TokenData tokenData = new TokenData(enclave, authorization, token);
if (tokenData.getTriesRemaining() > 0) {
Log.i(TAG, "Found data! " + enclave.getEnclaveName());
return tokenData;
} else if (firstKnownTokenData == null) {
Log.i(TAG, "No data, but storing as the first response. " + enclave.getEnclaveName());
firstKnownTokenData = tokenData;
} else {
Log.i(TAG, "No data, and we already have a 'first response'. " + enclave.getEnclaveName());
}
}
return Objects.requireNonNull(firstKnownTokenData);
}
private static void backupAuthToken(String token) {
final boolean tokenIsNew = SignalStore.kbsValues().appendAuthTokenToList(token);
if (tokenIsNew && SignalStore.kbsValues().hasPin()) {
new BackupManager(ApplicationDependencies.getApplication()).dataChanged();
}
}
/**
* Invoked during registration to restore the master key based on the server response during
* verification.
*
* Does not affect {@link PinState}.
*/
public static synchronized @Nullable KbsPinData restoreMasterKey(@Nullable String pin,
@NonNull KbsEnclave enclave,
@Nullable String basicStorageCredentials,
@NonNull TokenResponse tokenResponse)
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
{
Log.i(TAG, "restoreMasterKey()");
if (pin == null) return null;
if (basicStorageCredentials == null) {
throw new AssertionError("Cannot restore KBS key, no storage credentials supplied. Enclave: " + enclave.getEnclaveName());
}
Log.i(TAG, "Preparing to restore from " + enclave.getEnclaveName());
return restoreMasterKeyFromEnclave(enclave, pin, basicStorageCredentials, tokenResponse);
}
private static @NonNull KbsPinData restoreMasterKeyFromEnclave(@NonNull KbsEnclave enclave,
@NonNull String pin,
@NonNull String basicStorageCredentials,
@NonNull TokenResponse tokenResponse)
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
{
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(enclave);
KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials, tokenResponse);
try {
Log.i(TAG, "Restoring pin from KBS");
PinHash hashedPin = PinHashUtil.hashPin(pin, session.hashSalt());
KbsPinData kbsData = session.restorePin(hashedPin);
if (kbsData != null) {
Log.i(TAG, "Found registration lock token on KBS.");
} else {
throw new AssertionError("Null not expected");
}
return kbsData;
} catch (UnauthenticatedResponseException | InvalidKeyException e) {
Log.w(TAG, "Failed to restore key", e);
throw new IOException(e);
} catch (KeyBackupServicePinException e) {
Log.w(TAG, "Incorrect pin", e);
throw new KeyBackupSystemWrongPinException(e.getToken());
}
}
}

View File

@@ -1,18 +0,0 @@
package org.thoughtcrime.securesms.pin;
import androidx.annotation.NonNull;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
public final class KeyBackupSystemWrongPinException extends Exception {
private final TokenResponse tokenResponse;
public KeyBackupSystemWrongPinException(@NonNull TokenResponse tokenResponse){
this.tokenResponse = tokenResponse;
}
public @NonNull TokenResponse getTokenResponse() {
return tokenResponse;
}
}

View File

@@ -29,7 +29,7 @@ public final class PinOptOutDialog {
AlertDialog progress = SimpleProgressDialog.show(context);
SimpleTask.run(() -> {
PinState.onPinOptOut();
SvrRepository.optOutOfPin();
return null;
}, success -> {
Log.i(TAG, "Disable operation finished.");

View File

@@ -9,7 +9,7 @@ import androidx.appcompat.app.AppCompatActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -32,7 +32,7 @@ public final class PinRestoreActivity extends AppCompatActivity {
void navigateToPinCreation() {
final Intent main = MainActivity.clearTop(this);
final Intent createPin = CreateKbsPinActivity.getIntentForPinCreate(this);
final Intent createPin = CreateSvrPinActivity.getIntentForPinCreate(this);
final Intent chained = PassphraseRequiredActivity.chainIntent(createPin, main);
startActivity(chained);

View File

@@ -29,7 +29,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.KbsConstants;
import org.thoughtcrime.securesms.lock.v2.SvrConstants;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
@@ -113,7 +113,7 @@ public class PinRestoreEntryFragment extends LoggingFragment {
private void initViewModel() {
viewModel = new ViewModelProvider(this).get(PinRestoreViewModel.class);
viewModel.getTriesRemaining().observe(getViewLifecycleOwner(), this::presentTriesRemaining);
viewModel.triesRemaining.observe(getViewLifecycleOwner(), this::presentTriesRemaining);
viewModel.getEvent().observe(getViewLifecycleOwner(), this::presentEvent);
}
@@ -194,9 +194,9 @@ public class PinRestoreEntryFragment extends LoggingFragment {
private void onNeedHelpClicked() {
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PinRestoreEntryFragment_need_help)
.setMessage(getString(R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code, KbsConstants.MINIMUM_PIN_LENGTH))
.setMessage(getString(R.string.PinRestoreEntryFragment_your_pin_is_a_d_digit_code, SvrConstants.MINIMUM_PIN_LENGTH))
.setPositiveButton(R.string.PinRestoreEntryFragment_create_new_pin, ((dialog, which) -> {
PinState.onPinRestoreForgottenOrSkipped();
SvrRepository.onPinRestoreForgottenOrSkipped();
((PinRestoreActivity) requireActivity()).navigateToPinCreation();
}))
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> {
@@ -218,7 +218,7 @@ public class PinRestoreEntryFragment extends LoggingFragment {
.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();
SvrRepository.onPinRestoreForgottenOrSkipped();
((PinRestoreActivity) requireActivity()).navigateToPinCreation();
})
.setNegativeButton(R.string.PinRestoreEntryFragment_cancel, null)
@@ -226,7 +226,7 @@ public class PinRestoreEntryFragment extends LoggingFragment {
}
private void onAccountLocked() {
PinState.onPinRestoreForgottenOrSkipped();
SvrRepository.onPinRestoreForgottenOrSkipped();
SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), PinRestoreEntryFragmentDirections.actionAccountLocked());
}

View File

@@ -25,7 +25,7 @@ public class PinRestoreLockedFragment extends LoggingFragment {
View learnMoreButton = view.findViewById(R.id.pin_locked_learn_more);
createPinButton.setOnClickListener(v -> {
PinState.onPinRestoreForgottenOrSkipped();
SvrRepository.onPinRestoreForgottenOrSkipped();
((PinRestoreActivity) requireActivity()).navigateToPinCreation();
});

View File

@@ -1,83 +0,0 @@
package org.thoughtcrime.securesms.pin;
import androidx.annotation.NonNull;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.NewRegistrationUsernameSyncJob;
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.signal.core.util.Stopwatch;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
public class PinRestoreRepository {
private static final String TAG = Log.tag(PinRestoreRepository.class);
private final Executor executor = SignalExecutors.UNBOUNDED;
void submitPin(@NonNull String pin, @NonNull TokenData tokenData, @NonNull Callback<PinResultData> callback) {
executor.execute(() -> {
try {
Stopwatch stopwatch = new Stopwatch("PinSubmission");
KbsPinData kbsData = KbsRepository.restoreMasterKey(pin, tokenData.getEnclave(), tokenData.getBasicAuth(), tokenData.getTokenResponse());
PinState.onSignalPinRestore(ApplicationDependencies.getApplication(), Objects.requireNonNull(kbsData), pin);
stopwatch.split("MasterKey");
ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
stopwatch.split("AccountRestore");
ApplicationDependencies
.getJobManager()
.startChain(new StorageSyncJob())
.then(new NewRegistrationUsernameSyncJob())
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10));
stopwatch.split("ContactRestore");
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, TokenData.withResponse(tokenData, e.getTokenResponse())));
}
});
}
interface Callback<T> {
void onComplete(@NonNull T value);
}
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
}
}

View File

@@ -1,117 +0,0 @@
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 final KbsRepository kbsRepository;
private volatile TokenData tokenData;
public PinRestoreViewModel() {
this.repo = new PinRestoreRepository();
this.kbsRepository = new KbsRepository();
this.triesRemaining = new DefaultValueLiveData<>(new TriesRemaining(10, false));
this.event = new SingleLiveEvent<>();
kbsRepository.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.storageService().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 {
kbsRepository.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 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;
}
}
}

View File

@@ -0,0 +1,81 @@
package org.thoughtcrime.securesms.pin
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.util.DefaultValueLiveData
import org.thoughtcrime.securesms.util.SingleLiveEvent
import org.whispersystems.signalservice.api.svr.SecureValueRecovery
class PinRestoreViewModel : ViewModel() {
private val repo: SvrRepository = SvrRepository
@JvmField
val triesRemaining: DefaultValueLiveData<TriesRemaining> = DefaultValueLiveData(TriesRemaining(10, false))
private val event: SingleLiveEvent<Event> = SingleLiveEvent()
private val disposables = CompositeDisposable()
fun onPinSubmitted(pin: String, pinKeyboardType: PinKeyboardType) {
val trimmedLength = pin.trim().length
if (trimmedLength == 0) {
event.postValue(Event.EMPTY_PIN)
return
}
if (trimmedLength < SvrConstants.MINIMUM_PIN_LENGTH) {
event.postValue(Event.PIN_TOO_SHORT)
return
}
disposables += Single
.fromCallable { repo.restoreMasterKeyPostRegistration(pin, pinKeyboardType) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { result ->
when (result) {
is SecureValueRecovery.RestoreResponse.Success -> {
event.postValue(Event.SUCCESS)
}
is SecureValueRecovery.RestoreResponse.PinMismatch -> {
event.postValue(Event.PIN_INCORRECT)
triesRemaining.postValue(TriesRemaining(result.triesRemaining, true))
}
SecureValueRecovery.RestoreResponse.Missing -> {
event.postValue(Event.PIN_LOCKED)
}
is SecureValueRecovery.RestoreResponse.NetworkError -> {
event.postValue(Event.NETWORK_ERROR)
}
is SecureValueRecovery.RestoreResponse.ApplicationError -> {
event.postValue(Event.NETWORK_ERROR)
}
}
}
}
fun getEvent(): LiveData<Event> {
return event
}
enum class Event {
SUCCESS,
EMPTY_PIN,
PIN_TOO_SHORT,
PIN_INCORRECT,
PIN_LOCKED,
NETWORK_ERROR
}
class TriesRemaining(val count: Int, private val hasIncorrectGuess: Boolean) {
fun hasIncorrectGuess(): Boolean {
return hasIncorrectGuess
}
}
}

View File

@@ -1,435 +0,0 @@
package org.thoughtcrime.securesms.pin;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.svr2.PinHash;
import org.thoughtcrime.securesms.KbsEnclave;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.JobTracker;
import org.thoughtcrime.securesms.jobs.ClearFallbackKbsEnclaveJob;
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.RegistrationLockReminders;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.kbs.PinHashUtil;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import java.io.IOException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
public final class PinState {
private static final String TAG = Log.tag(PinState.class);
/**
* Invoked after a user has successfully registered. Ensures all the necessary state is updated.
*/
public static synchronized void onRegistration(@NonNull Context context,
@Nullable KbsPinData kbsData,
@Nullable String pin,
boolean hasPinToRestore,
boolean setRegistrationLockEnabled)
{
Log.i(TAG, "onRegistration()");
TextSecurePreferences.setV1RegistrationLockPin(context, pin);
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) {
if (setRegistrationLockEnabled) {
Log.i(TAG, "Registration Lock V2");
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
} else {
Log.i(TAG, "ReRegistration Skip SMS");
}
SignalStore.kbsValues().setKbsMasterKey(kbsData, pin);
SignalStore.pinValues().resetPinReminders();
resetPinRetryCount(context, pin);
ClearFallbackKbsEnclaveJob.clearAll();
} else if (hasPinToRestore) {
Log.i(TAG, "Has a PIN to restore.");
SignalStore.kbsValues().clearRegistrationLockAndPin();
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
SignalStore.storageService().setNeedsAccountRestore(true);
} else {
Log.i(TAG, "No registration lock or PIN at all.");
SignalStore.kbsValues().clearRegistrationLockAndPin();
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
}
}
/**
* 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();
SignalStore.kbsValues().setPinForgottenOrSkipped(false);
SignalStore.storageService().setNeedsAccountRestore(false);
resetPinRetryCount(context, pin);
ClearFallbackKbsEnclaveJob.clearAll();
}
/**
* 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.storageService().setNeedsAccountRestore(false);
SignalStore.kbsValues().setPinForgottenOrSkipped(true);
}
/**
* Invoked whenever the Signal PIN is changed or created.
*/
@WorkerThread
public static synchronized void onPinChangedOrCreated(@NonNull Context context, @NonNull String pin, @NonNull PinKeyboardType keyboard)
throws IOException, UnauthenticatedResponseException, InvalidKeyException
{
Log.i(TAG, "onPinChangedOrCreated()");
KbsEnclave kbsEnclave = KbsEnclaves.current();
KbsValues kbsValues = SignalStore.kbsValues();
boolean isFirstPin = !kbsValues.hasPin() || kbsValues.hasOptedOut();
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(kbsEnclave);
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
PinHash pinHash = PinHashUtil.hashPin(pin, pinChangeSession.hashSalt());
KbsPinData kbsData = pinChangeSession.setPin(pinHash, masterKey);
kbsValues.setKbsMasterKey(kbsData, pin);
kbsValues.setPinForgottenOrSkipped(false);
TextSecurePreferences.clearRegistrationLockV1(context);
SignalStore.pinValues().setKeyboardType(keyboard);
SignalStore.pinValues().resetPinReminders();
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL);
if (isFirstPin) {
Log.i(TAG, "First time setting a PIN. Refreshing attributes to set the 'storage' capability. Enclave: " + kbsEnclave.getEnclaveName());
bestEffortRefreshAttributes();
} else {
Log.i(TAG, "Not the first time setting a PIN. Enclave: " + kbsEnclave.getEnclaveName());
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
}
}
/**
* 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() {
Log.i(TAG, "onPinOptOutEnabled()");
assertState(State.PIN_WITH_REGISTRATION_LOCK_DISABLED, State.NO_REGISTRATION_LOCK);
optOutOfPin();
}
/**
* Invoked whenever a Signal PIN user enables registration lock.
*/
@WorkerThread
public static synchronized void onEnableRegistrationLockForUserWithPin() throws IOException {
Log.i(TAG, "onEnableRegistrationLockForUserWithPin()");
if (getState() == State.PIN_WITH_REGISTRATION_LOCK_ENABLED) {
Log.i(TAG, "Registration lock already enabled. Skipping.");
return;
}
assertState(State.PIN_WITH_REGISTRATION_LOCK_DISABLED);
KbsEnclave kbsEnclave = KbsEnclaves.current();
Log.i(TAG, "Enclave: " + kbsEnclave.getEnclaveName());
SignalStore.kbsValues().setV2RegistrationLockEnabled(false);
ApplicationDependencies.getKeyBackupService(kbsEnclave)
.newPinChangeSession(SignalStore.kbsValues().getRegistrationLockTokenResponse())
.enableRegistrationLock(SignalStore.kbsValues().getOrCreateMasterKey());
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
}
/**
* Invoked whenever a Signal PIN user disables registration lock.
*/
@WorkerThread
public static synchronized void onDisableRegistrationLockForUserWithPin() throws IOException {
Log.i(TAG, "onDisableRegistrationLockForUserWithPin()");
if (getState() == State.PIN_WITH_REGISTRATION_LOCK_DISABLED) {
Log.i(TAG, "Registration lock already disabled. Skipping.");
return;
}
assertState(State.PIN_WITH_REGISTRATION_LOCK_ENABLED);
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
ApplicationDependencies.getKeyBackupService(KbsEnclaves.current())
.newPinChangeSession(SignalStore.kbsValues().getRegistrationLockTokenResponse())
.disableRegistrationLock();
SignalStore.kbsValues().setV2RegistrationLockEnabled(false);
}
/**
* 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, InvalidKeyException
{
Log.i(TAG, "onMigrateToRegistrationLockV2()");
KbsEnclave kbsEnclave = KbsEnclaves.current();
Log.i(TAG, "Enclave: " + kbsEnclave.getEnclaveName());
KbsValues kbsValues = SignalStore.kbsValues();
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(kbsEnclave);
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
PinHash pinHash = PinHashUtil.hashPin(pin, pinChangeSession.hashSalt());
KbsPinData kbsData = pinChangeSession.setPin(pinHash, masterKey);
pinChangeSession.enableRegistrationLock(masterKey);
kbsValues.setKbsMasterKey(kbsData, pin);
TextSecurePreferences.clearRegistrationLockV1(context);
}
/**
* Should only be called by {@link org.thoughtcrime.securesms.jobs.KbsEnclaveMigrationWorkerJob}.
*/
@WorkerThread
public static synchronized void onMigrateToNewEnclave(@NonNull String pin)
throws IOException, UnauthenticatedResponseException
{
Log.i(TAG, "onMigrateToNewEnclave()");
assertState(State.PIN_WITH_REGISTRATION_LOCK_DISABLED, State.PIN_WITH_REGISTRATION_LOCK_ENABLED);
Log.i(TAG, "Migrating to enclave " + KbsEnclaves.current().getEnclaveName());
setPinOnEnclave(KbsEnclaves.current(), pin, SignalStore.kbsValues().getOrCreateMasterKey());
ClearFallbackKbsEnclaveJob.clearAll();
}
@WorkerThread
private static void bestEffortRefreshAttributes() {
Optional<JobTracker.JobState> result = ApplicationDependencies.getJobManager().runSynchronously(new RefreshAttributesJob(), TimeUnit.SECONDS.toMillis(10));
if (result.isPresent() && result.get() == JobTracker.JobState.SUCCESS) {
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());
} else {
Log.w(TAG, "Job did not finish in the allotted time. It'll finish later.");
}
}
@WorkerThread
private static void bestEffortForcePushStorage() {
Optional<JobTracker.JobState> result = ApplicationDependencies.getJobManager().runSynchronously(new StorageForcePushJob(), TimeUnit.SECONDS.toMillis(10));
if (result.isPresent() && result.get() == JobTracker.JobState.SUCCESS) {
Log.i(TAG, "Storage was force-pushed successfully.");
} else if (result.isPresent()) {
Log.w(TAG, "Storage force-pushed finished, but was not successful. Enqueuing one for later. (Result: " + result.get() + ")");
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
} else {
Log.w(TAG, "Storage fore push did not finish in the allotted time. It'll finish later.");
}
}
@WorkerThread
private static void resetPinRetryCount(@NonNull Context context, @Nullable String pin) {
if (pin == null) {
return;
}
try {
setPinOnEnclave(KbsEnclaves.current(), pin, SignalStore.kbsValues().getOrCreateMasterKey());
TextSecurePreferences.clearRegistrationLockV1(context);
Log.i(TAG, "Pin set/attempts reset on KBS");
} catch (IOException e) {
Log.w(TAG, "May have failed to reset pin attempts!", e);
} catch (UnauthenticatedResponseException e) {
Log.w(TAG, "Failed to reset pin attempts", e);
}
}
@WorkerThread
private static @NonNull KbsPinData setPinOnEnclave(@NonNull KbsEnclave enclave, @NonNull String pin, @NonNull MasterKey masterKey)
throws IOException, UnauthenticatedResponseException
{
Log.i(TAG, "Setting PIN on enclave: " + enclave.getEnclaveName());
KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave);
KeyBackupService.PinChangeSession pinChangeSession = kbs.newPinChangeSession();
PinHash pinHash = PinHashUtil.hashPin(pin, pinChangeSession.hashSalt());
KbsPinData newData = pinChangeSession.setPin(pinHash, masterKey);
SignalStore.kbsValues().setKbsMasterKey(newData, pin);
return newData;
}
@WorkerThread
private static void optOutOfPin() {
SignalStore.kbsValues().optOut();
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL);
bestEffortRefreshAttributes();
bestEffortForcePushStorage();
}
private static @NonNull State assertState(State... allowed) {
State currentState = getState();
for (State state : allowed) {
if (currentState == state) {
return currentState;
}
}
switch (currentState) {
case NO_REGISTRATION_LOCK: throw new InvalidState_NoRegistrationLock();
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);
}
}
public static @NonNull State getState() {
Context context = ApplicationDependencies.getApplication();
KbsValues kbsValues = SignalStore.kbsValues();
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;
}
if (v1Enabled && !v2Enabled && !hasPin) {
return State.REGISTRATION_LOCK_V1;
}
if (v2Enabled && hasPin) {
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
return State.PIN_WITH_REGISTRATION_LOCK_ENABLED;
}
if (!v2Enabled && hasPin) {
TextSecurePreferences.setV1RegistrationLockEnabled(context, false);
return State.PIN_WITH_REGISTRATION_LOCK_DISABLED;
}
throw new InvalidInferredStateError(String.format(Locale.ENGLISH, "Invalid state! v1: %b, v2: %b, pin: %b", v1Enabled, v2Enabled, hasPin));
}
private enum State {
/**
* User has nothing -- either in the process of registration, or pre-PIN-migration
*/
NO_REGISTRATION_LOCK("no_registration_lock"),
/**
* User has a V1 registration lock set
*/
REGISTRATION_LOCK_V1("registration_lock_v1"),
/**
* User has a PIN, and registration lock is enabled.
*/
PIN_WITH_REGISTRATION_LOCK_ENABLED("pin_with_registration_lock_enabled"),
/**
* User has a PIN, but registration lock is 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
* serialization.
*/
private final String key;
State(String key) {
this.key = key;
}
public @NonNull String serialize() {
return key;
}
public static @NonNull State deserialize(@NonNull String serialized) {
for (State state : values()) {
if (state.key.equals(serialized)) {
return state;
}
}
throw new IllegalArgumentException("No state for value: " + serialized);
}
}
private static class InvalidInferredStateError extends Error {
InvalidInferredStateError(String message) {
super(message);
}
}
private static class InvalidState_NoRegistrationLock extends IllegalStateException {}
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 {}
}

View File

@@ -45,7 +45,7 @@ public final class RegistrationLockV2Dialog {
SimpleTask.run(SignalExecutors.UNBOUNDED, () -> {
try {
PinState.onEnableRegistrationLockForUserWithPin();
SvrRepository.enableRegistrationLockForUserWithPin();
Log.i(TAG, "Successfully enabled registration lock.");
return true;
} catch (IOException e) {
@@ -87,7 +87,7 @@ public final class RegistrationLockV2Dialog {
SimpleTask.run(SignalExecutors.UNBOUNDED, () -> {
try {
PinState.onDisableRegistrationLockForUserWithPin();
SvrRepository.disableRegistrationLockForUserWithPin();
Log.i(TAG, "Successfully disabled registration lock.");
return true;
} catch (IOException e) {

View File

@@ -0,0 +1,411 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.pin
import android.app.backup.BackupManager
import androidx.annotation.WorkerThread
import org.signal.core.util.Stopwatch
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.jobs.NewRegistrationUsernameSyncJob
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.jobs.ResetSvrGuessCountJob
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
import org.thoughtcrime.securesms.jobs.StorageSyncJob
import org.thoughtcrime.securesms.jobs.Svr2MirrorJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.SvrNoDataException
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.svr.SecureValueRecovery
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.BackupResponse
import org.whispersystems.signalservice.api.svr.SecureValueRecovery.RestoreResponse
import org.whispersystems.signalservice.api.svr.SecureValueRecoveryV1
import org.whispersystems.signalservice.internal.push.AuthCredentials
import java.io.IOException
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
object SvrRepository {
val TAG = Log.tag(SvrRepository::class.java)
private val svr2: SecureValueRecovery = ApplicationDependencies.getSignalServiceAccountManager().getSecureValueRecoveryV2(BuildConfig.SVR2_MRENCLAVE)
private val svr1: SecureValueRecovery = SecureValueRecoveryV1(ApplicationDependencies.getKeyBackupService(BuildConfig.KBS_ENCLAVE))
/** An ordered list of SVR implementations. They should be in priority order, with the most important one listed first. */
private val implementations: List<SecureValueRecovery> = listOf(svr2, svr1)
/**
* A lock that ensures that only one thread at a time is altering the various pieces of SVR state.
*
* External usage of this should be limited to one-time migrations. Any routine operation that needs the lock should go in
* this repository instead.
*/
val operationLock = ReentrantLock()
/**
* Restores the master key from the first available SVR implementation available.
*
* This is intended to be called before registration has been completed, requiring
* that you pass in the credentials provided during registration to access SVR.
*
* You could be hitting this because the user has reglock (and therefore need to
* restore the master key before you can register), or you may be doing the
* sms-skip flow.
*/
@JvmStatic
@WorkerThread
@Throws(IOException::class, SvrWrongPinException::class, SvrNoDataException::class)
fun restoreMasterKeyPreRegistration(credentials: SvrAuthCredentialSet, userPin: String): MasterKey {
operationLock.withLock {
Log.i(TAG, "restoreMasterKeyPreRegistration()", true)
val operations: List<Pair<SecureValueRecovery, () -> RestoreResponse>> = listOf(
svr2 to { restoreMasterKeyPreRegistration(svr2, credentials.svr2, userPin) },
svr1 to { restoreMasterKeyPreRegistration(svr1, credentials.svr1, userPin) }
)
for ((implementation, operation) in operations) {
when (val response: RestoreResponse = operation()) {
is RestoreResponse.Success -> {
Log.i(TAG, "[restoreMasterKeyPreRegistration] Successfully restored master key. $implementation", true)
if (implementation == svr2) {
SignalStore.svr().appendAuthTokenToList(response.authorization.asBasic())
}
return response.masterKey
}
is RestoreResponse.PinMismatch -> {
Log.i(TAG, "[restoreMasterKeyPreRegistration] Incorrect PIN. $implementation", true)
throw SvrWrongPinException(response.triesRemaining)
}
is RestoreResponse.NetworkError -> {
Log.i(TAG, "[restoreMasterKeyPreRegistration] Network error. $implementation", response.exception, true)
throw response.exception
}
is RestoreResponse.ApplicationError -> {
Log.i(TAG, "[restoreMasterKeyPreRegistration] Application error. $implementation", response.exception, true)
throw IOException(response.exception)
}
RestoreResponse.Missing -> {
Log.w(TAG, "[restoreMasterKeyPreRegistration] No data found for $implementation | Continuing to next implementation.", true)
}
}
}
Log.w(TAG, "[restoreMasterKeyPreRegistration] No data found for any implementation!", true)
throw SvrNoDataException()
}
}
/**
* Restores the master key from the first available SVR implementation available.
*
* This is intended to be called after the user has registered, allowing the function
* to fetch credentials on its own.
*/
@WorkerThread
fun restoreMasterKeyPostRegistration(userPin: String, pinKeyboardType: PinKeyboardType): RestoreResponse {
val stopwatch = Stopwatch("pin-submission")
operationLock.withLock {
for (implementation in implementations) {
when (val response: RestoreResponse = implementation.restoreDataPostRegistration(userPin)) {
is RestoreResponse.Success -> {
Log.i(TAG, "[restoreMasterKeyPostRegistration] Successfully restored master key. $implementation", true)
stopwatch.split("restore")
SignalStore.svr().setMasterKey(response.masterKey, userPin)
SignalStore.svr().isRegistrationLockEnabled = false
SignalStore.pinValues().resetPinReminders()
SignalStore.svr().isPinForgottenOrSkipped = false
SignalStore.storageService().setNeedsAccountRestore(false)
SignalStore.pinValues().keyboardType = pinKeyboardType
SignalStore.storageService().setNeedsAccountRestore(false)
if (implementation == svr2) {
SignalStore.svr().appendAuthTokenToList(response.authorization.asBasic())
}
ApplicationDependencies.getJobManager().add(ResetSvrGuessCountJob())
stopwatch.split("metadata")
ApplicationDependencies.getJobManager().runSynchronously(StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN)
stopwatch.split("account-restore")
ApplicationDependencies
.getJobManager()
.startChain(StorageSyncJob())
.then(NewRegistrationUsernameSyncJob())
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10))
stopwatch.split("contact-restore")
if (implementation != svr2) {
ApplicationDependencies.getJobManager().add(Svr2MirrorJob())
}
stopwatch.stop(TAG)
return response
}
is RestoreResponse.PinMismatch -> {
Log.i(TAG, "[restoreMasterKeyPostRegistration] Incorrect PIN. $implementation", true)
return response
}
is RestoreResponse.NetworkError -> {
Log.i(TAG, "[restoreMasterKeyPostRegistration] Network error. $implementation", response.exception, true)
return response
}
is RestoreResponse.ApplicationError -> {
Log.i(TAG, "[restoreMasterKeyPostRegistration] Application error. $implementation", response.exception, true)
return response
}
RestoreResponse.Missing -> {
Log.w(TAG, "[restoreMasterKeyPostRegistration] No data found for: $implementation | Continuing to next implementation.", true)
}
}
}
Log.w(TAG, "[restoreMasterKeyPostRegistration] No data found for any implementation!", true)
return RestoreResponse.Missing
}
}
/**
* Sets the user's PIN the one specified, updating local stores as necessary.
* The resulting Single will not throw an error in any expected case, only if there's a runtime exception.
*/
@WorkerThread
@JvmStatic
fun setPin(userPin: String, keyboardType: PinKeyboardType): BackupResponse {
return operationLock.withLock {
val masterKey: MasterKey = SignalStore.svr().getOrCreateMasterKey()
val responses: List<BackupResponse> = implementations
.filter { it != svr2 || FeatureFlags.svr2() }
.map { it.setPin(userPin, masterKey) }
.map { it.execute() }
Log.i(TAG, "[setPin] Responses: $responses", true)
val error: BackupResponse? = responses.map {
when (it) {
is BackupResponse.ApplicationError -> it
BackupResponse.ExposeFailure -> it
is BackupResponse.NetworkError -> it
BackupResponse.ServerRejected -> it
BackupResponse.EnclaveNotFound -> null
is BackupResponse.Success -> null
}
}.firstOrNull()
val overallResponse = error
?: responses.firstOrNull { it is BackupResponse.Success }
?: responses[0]
if (overallResponse is BackupResponse.Success) {
Log.i(TAG, "[setPin] Success!", true)
SignalStore.svr().setMasterKey(masterKey, userPin)
SignalStore.svr().isPinForgottenOrSkipped = false
SignalStore.svr().appendAuthTokenToList(overallResponse.authorization.asBasic())
SignalStore.pinValues().keyboardType = keyboardType
SignalStore.pinValues().resetPinReminders()
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL)
ApplicationDependencies.getJobManager().add(RefreshAttributesJob())
} else {
Log.w(TAG, "[setPin] Failed to set PIN! $overallResponse", true)
if (hasNoRegistrationLock) {
SignalStore.svr().onPinCreateFailure()
}
}
overallResponse
}
}
/**
* Invoked after a user has successfully registered. Ensures all the necessary state is updated.
*/
@WorkerThread
@JvmStatic
fun onRegistrationComplete(
masterKey: MasterKey?,
userPin: String?,
hasPinToRestore: Boolean,
setRegistrationLockEnabled: Boolean
) {
Log.i(TAG, "[onRegistrationComplete] Starting", true)
operationLock.withLock {
if (masterKey == null && userPin != null) {
error("If masterKey is present, pin must also be present!")
}
if (masterKey != null && userPin != null) {
if (setRegistrationLockEnabled) {
Log.i(TAG, "[onRegistrationComplete] Registration Lock", true)
SignalStore.svr().isRegistrationLockEnabled = true
} else {
Log.i(TAG, "[onRegistrationComplete] ReRegistration Skip SMS", true)
}
SignalStore.svr().setMasterKey(masterKey, userPin)
SignalStore.pinValues().resetPinReminders()
ApplicationDependencies.getJobManager().add(ResetSvrGuessCountJob())
} else if (hasPinToRestore) {
Log.i(TAG, "[onRegistrationComplete] Has a PIN to restore.", true)
SignalStore.svr().clearRegistrationLockAndPin()
SignalStore.storageService().setNeedsAccountRestore(true)
} else {
Log.i(TAG, "[onRegistrationComplete] No registration lock or PIN at all.", true)
SignalStore.svr().clearRegistrationLockAndPin()
}
}
ApplicationDependencies.getJobManager().add(RefreshAttributesJob())
}
/**
* Invoked when the user skips out on PIN restoration or otherwise fails to remember their PIN.
*/
@JvmStatic
fun onPinRestoreForgottenOrSkipped() {
operationLock.withLock {
SignalStore.svr().clearRegistrationLockAndPin()
SignalStore.storageService().setNeedsAccountRestore(false)
SignalStore.svr().isPinForgottenOrSkipped = true
}
}
@JvmStatic
@WorkerThread
fun optOutOfPin() {
operationLock.withLock {
SignalStore.svr().optOut()
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.PINS_FOR_ALL)
bestEffortRefreshAttributes()
bestEffortForcePushStorage()
}
}
@JvmStatic
@WorkerThread
@Throws(IOException::class)
fun enableRegistrationLockForUserWithPin() {
operationLock.withLock {
check(SignalStore.svr().hasPin() && !SignalStore.svr().hasOptedOut()) { "Must have a PIN to set a registration lock!" }
Log.i(TAG, "[enableRegistrationLockForUserWithPin] Enabling registration lock.", true)
ApplicationDependencies.getSignalServiceAccountManager().enableRegistrationLock(SignalStore.svr().getOrCreateMasterKey())
SignalStore.svr().isRegistrationLockEnabled = true
Log.i(TAG, "[enableRegistrationLockForUserWithPin] Registration lock successfully enabled.", true)
}
}
@JvmStatic
@WorkerThread
@Throws(IOException::class)
fun disableRegistrationLockForUserWithPin() {
operationLock.withLock {
check(SignalStore.svr().hasPin() && !SignalStore.svr().hasOptedOut()) { "Must have a PIN to disable registration lock!" }
Log.i(TAG, "[disableRegistrationLockForUserWithPin] Disabling registration lock.", true)
ApplicationDependencies.getSignalServiceAccountManager().disableRegistrationLock()
SignalStore.svr().isRegistrationLockEnabled = false
Log.i(TAG, "[disableRegistrationLockForUserWithPin] Registration lock successfully disabled.", true)
}
}
/**
* Fetches new SVR credentials and persists them in the backup store to be used during re-registration.
*/
@WorkerThread
@Throws(IOException::class)
fun refreshAndStoreAuthorization() {
try {
val credentials: AuthCredentials = svr2.authorization()
backupSvrCredentials(credentials)
} catch (e: Throwable) {
if (e is IOException) {
throw e
} else {
throw IOException(e)
}
}
}
@WorkerThread
private fun restoreMasterKeyPreRegistration(svr: SecureValueRecovery, credentials: AuthCredentials?, userPin: String): RestoreResponse {
return if (credentials == null) {
RestoreResponse.Missing
} else {
svr.restoreDataPreRegistration(credentials, userPin)
}
}
@WorkerThread
private fun bestEffortRefreshAttributes() {
val result = ApplicationDependencies.getJobManager().runSynchronously(RefreshAttributesJob(), TimeUnit.SECONDS.toMillis(10))
if (result.isPresent && result.get() == JobTracker.JobState.SUCCESS) {
Log.i(TAG, "Attributes were refreshed successfully.", true)
} else if (result.isPresent) {
Log.w(TAG, "Attribute refresh finished, but was not successful. Enqueuing one for later. (Result: " + result.get() + ")", true)
ApplicationDependencies.getJobManager().add(RefreshAttributesJob())
} else {
Log.w(TAG, "Job did not finish in the allotted time. It'll finish later.", true)
}
}
@WorkerThread
private fun bestEffortForcePushStorage() {
val result = ApplicationDependencies.getJobManager().runSynchronously(StorageForcePushJob(), TimeUnit.SECONDS.toMillis(10))
if (result.isPresent && result.get() == JobTracker.JobState.SUCCESS) {
Log.i(TAG, "Storage was force-pushed successfully.", true)
} else if (result.isPresent) {
Log.w(TAG, "Storage force-pushed finished, but was not successful. Enqueuing one for later. (Result: " + result.get() + ")", true)
ApplicationDependencies.getJobManager().add(RefreshAttributesJob())
} else {
Log.w(TAG, "Storage fore push did not finish in the allotted time. It'll finish later.", true)
}
}
private fun backupSvrCredentials(credentials: AuthCredentials) {
val tokenIsNew = SignalStore.svr().appendAuthTokenToList(credentials.asBasic())
if (tokenIsNew && SignalStore.svr().hasPin()) {
BackupManager(ApplicationDependencies.getApplication()).dataChanged()
}
}
private val hasNoRegistrationLock: Boolean
get() {
return !SignalStore.svr().isRegistrationLockEnabled &&
!SignalStore.svr().hasPin() &&
!SignalStore.svr().hasOptedOut()
}
}

View File

@@ -0,0 +1,14 @@
package org.thoughtcrime.securesms.pin;
public final class SvrWrongPinException extends Exception {
private final int triesRemaining;
public SvrWrongPinException(int triesRemaining){
this.triesRemaining = triesRemaining;
}
public int getTriesRemaining() {
return triesRemaining;
}
}

View File

@@ -1,82 +0,0 @@
package org.thoughtcrime.securesms.pin;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.KbsEnclave;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
public class TokenData implements Parcelable {
private final KbsEnclave enclave;
private final String basicAuth;
private final TokenResponse tokenResponse;
TokenData(@NonNull KbsEnclave enclave, @NonNull String basicAuth, @NonNull TokenResponse tokenResponse) {
this.enclave = enclave;
this.basicAuth = basicAuth;
this.tokenResponse = tokenResponse;
}
private TokenData(Parcel in) {
this.enclave = new KbsEnclave(in.readString(), in.readString(), in.readString());
this.basicAuth = in.readString();
byte[] backupId = in.createByteArray();
byte[] token = in.createByteArray();
this.tokenResponse = new TokenResponse(backupId, token, in.readInt());
}
public static @NonNull TokenData withResponse(@NonNull TokenData data, @NonNull TokenResponse response) {
return new TokenData(data.getEnclave(), data.getBasicAuth(), response);
}
public int getTriesRemaining() {
return tokenResponse.getTries();
}
public @NonNull String getBasicAuth() {
return basicAuth;
}
public @NonNull TokenResponse getTokenResponse() {
return tokenResponse;
}
public @NonNull KbsEnclave getEnclave() {
return enclave;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(enclave.getEnclaveName());
dest.writeString(enclave.getServiceId());
dest.writeString(enclave.getMrEnclave());
dest.writeString(basicAuth);
dest.writeByteArray(tokenResponse.getBackupId());
dest.writeByteArray(tokenResponse.getToken());
dest.writeInt(tokenResponse.getTries());
}
public static final Creator<TokenData> CREATOR = new Creator<TokenData>() {
@Override
public TokenData createFromParcel(Parcel in) {
return new TokenData(in);
}
@Override
public TokenData[] newArray(int size) {
return new TokenData[size];
}
};
}