mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
Start mirroring to SVR2.
This commit is contained in:
committed by
Clark Chen
parent
dfb7304626
commit
e1570e9512
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user