diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java new file mode 100644 index 0000000000..7fcc73b017 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java @@ -0,0 +1,141 @@ +package org.thoughtcrime.securesms.pin; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.signal.core.util.concurrent.SignalExecutors; +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.KbsEnclave; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.lock.PinHashing; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.util.guava.Optional; +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.HashedPin; +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.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 final class KbsRepository { + + private static final String TAG = Log.tag(KbsRepository.class); + + public void getToken(@NonNull Consumer> callback) { + SignalExecutors.UNBOUNDED.execute(() -> { + try { + callback.accept(Optional.fromNullable(getTokenSync(null))); + } catch (IOException e) { + callback.accept(Optional.absent()); + } + }); + } + + /** + * @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> getToken(@Nullable String authorization) { + return Single.>fromCallable(() -> { + try { + return ServiceResponse.forResult(getTokenSync(authorization), 200, null); + } catch (IOException e) { + return ServiceResponse.forUnknownError(e); + } + }).subscribeOn(Schedulers.io()); + } + + private @NonNull TokenData getTokenSync(@Nullable String authorization) throws IOException { + TokenData firstKnownTokenData = null; + + for (KbsEnclave enclave : KbsEnclaves.all()) { + KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave); + + authorization = authorization == null ? kbs.getAuthorization() : authorization; + + TokenResponse token = kbs.getToken(authorization); + 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); + } + + /** + * 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"); + } + + 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"); + + HashedPin hashedPin = PinHashing.hashPin(pin, session); + 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()); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java b/app/src/main/java/org/thoughtcrime/securesms/pin/KeyBackupSystemWrongPinException.java similarity index 88% rename from app/src/main/java/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java rename to app/src/main/java/org/thoughtcrime/securesms/pin/KeyBackupSystemWrongPinException.java index 4c0c52ba9d..ac8365be08 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/KeyBackupSystemWrongPinException.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/KeyBackupSystemWrongPinException.java @@ -1,4 +1,4 @@ -package org.thoughtcrime.securesms.registration.service; +package org.thoughtcrime.securesms.pin; import androidx.annotation.NonNull; diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java index 9ab5d810f4..59421aeaac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java @@ -1,24 +1,15 @@ package org.thoughtcrime.securesms.pin; -import android.os.Parcel; -import android.os.Parcelable; - import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.KbsEnclave; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob; import org.thoughtcrime.securesms.jobs.StorageSyncJob; -import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException; import org.thoughtcrime.securesms.util.Stopwatch; -import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.KbsPinData; -import org.whispersystems.signalservice.api.KeyBackupService; import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; -import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; import java.io.IOException; import java.util.Objects; @@ -31,52 +22,12 @@ public class PinRestoreRepository { private final Executor executor = SignalExecutors.UNBOUNDED; - void getToken(@NonNull Callback> callback) { - executor.execute(() -> { - try { - callback.onComplete(Optional.fromNullable(getTokenSync(null))); - } catch (IOException e) { - callback.onComplete(Optional.absent()); - } - }); - } - - /** - * @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 @NonNull TokenData getTokenSync(@Nullable String authorization) throws IOException { - TokenData firstKnownTokenData = null; - - for (KbsEnclave enclave : KbsEnclaves.all()) { - KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave); - - authorization = authorization == null ? kbs.getAuthorization() : authorization; - - TokenResponse token = kbs.getToken(authorization); - 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); - } - void submitPin(@NonNull String pin, @NonNull TokenData tokenData, @NonNull Callback callback) { executor.execute(() -> { try { Stopwatch stopwatch = new Stopwatch("PinSubmission"); - KbsPinData kbsData = PinState.restoreMasterKey(pin, tokenData.getEnclave(), tokenData.getBasicAuth(), tokenData.getTokenResponse()); + KbsPinData kbsData = KbsRepository.restoreMasterKey(pin, tokenData.getEnclave(), tokenData.getBasicAuth(), tokenData.getTokenResponse()); PinState.onSignalPinRestore(ApplicationDependencies.getApplication(), Objects.requireNonNull(kbsData), pin); stopwatch.split("MasterKey"); @@ -103,83 +54,6 @@ public class PinRestoreRepository { void onComplete(@NonNull T value); } - public static 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) { - //noinspection ConstantConditions - this.enclave = new KbsEnclave(in.readString(), in.readString(), in.readString()); - this.basicAuth = in.readString(); - - byte[] backupId = new byte[0]; - byte[] token = new byte[0]; - - in.readByteArray(backupId); - in.readByteArray(token); - - 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 CREATOR = new Creator() { - @Override - public TokenData createFromParcel(Parcel in) { - return new TokenData(in); - } - - @Override - public TokenData[] newArray(int size) { - return new TokenData[size]; - } - }; - - } - static class PinResultData { private final PinResult result; private final TokenData tokenData; diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java index 0efa190e04..7b8f6c4598 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java @@ -15,15 +15,17 @@ public class PinRestoreViewModel extends ViewModel { private final PinRestoreRepository repo; private final DefaultValueLiveData triesRemaining; private final SingleLiveEvent event; + private final KbsRepository kbsRepository; - private volatile PinRestoreRepository.TokenData tokenData; + 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<>(); - repo.getToken(token -> { + kbsRepository.getToken(token -> { if (token.isPresent()) { updateTokenData(token.get(), false); } else { @@ -67,7 +69,7 @@ public class PinRestoreViewModel extends ViewModel { } }); } else { - repo.getToken(token -> { + kbsRepository.getToken(token -> { if (token.isPresent()) { updateTokenData(token.get(), false); onPinSubmitted(pin, pinKeyboardType); @@ -86,7 +88,7 @@ public class PinRestoreViewModel extends ViewModel { return event; } - private void updateTokenData(@NonNull PinRestoreRepository.TokenData tokenData, boolean incorrectGuess) { + private void updateTokenData(@NonNull TokenData tokenData, boolean incorrectGuess) { this.tokenData = tokenData; triesRemaining.postValue(new TriesRemaining(tokenData.getTriesRemaining(), incorrectGuess)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java index 84cb98d831..64e7245f26 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java @@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.lock.PinHashing; import org.thoughtcrime.securesms.lock.RegistrationLockReminders; import org.thoughtcrime.securesms.lock.v2.PinKeyboardType; import org.thoughtcrime.securesms.megaphone.Megaphones; -import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.guava.Optional; @@ -41,61 +40,6 @@ public final class PinState { private static final String TAG = Log.tag(PinState.class); - /** - * 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"); - } - - 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"); - - HashedPin hashedPin = PinHashing.hashPin(pin, session); - 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()); - } - } - /** * Invoked after a user has successfully registered. Ensures all the necessary state is updated. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/TokenData.java b/app/src/main/java/org/thoughtcrime/securesms/pin/TokenData.java new file mode 100644 index 0000000000..6abb3e9b6c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/TokenData.java @@ -0,0 +1,82 @@ +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 CREATOR = new Creator() { + @Override + public TokenData createFromParcel(Parcel in) { + return new TokenData(in); + } + + @Override + public TokenData[] newArray(int size) { + return new TokenData[size]; + } + }; + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt new file mode 100644 index 0000000000..313c6330e1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationData.kt @@ -0,0 +1,15 @@ +package org.thoughtcrime.securesms.registration + +import org.signal.zkgroup.profiles.ProfileKey + +data class RegistrationData( + val code: String, + val e164: String, + val password: String, + val registrationId: Int, + val profileKey: ProfileKey, + val fcmToken: String? +) { + val isFcm: Boolean = fcmToken != null + val isNotFcm: Boolean = fcmToken == null +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java index ce36718863..5e7385a704 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationNavigationActivity.java @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; +import androidx.lifecycle.ViewModelProviders; import com.google.android.gms.auth.api.phone.SmsRetriever; import com.google.android.gms.common.api.CommonStatusCodes; @@ -18,6 +19,7 @@ import com.google.android.gms.common.api.Status; import org.greenrobot.eventbus.EventBus; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import org.thoughtcrime.securesms.service.VerificationCodeParser; import org.thoughtcrime.securesms.util.CommunicationActions; import org.whispersystems.libsignal.util.guava.Optional; @@ -28,10 +30,9 @@ public final class RegistrationNavigationActivity extends AppCompatActivity { public static final String RE_REGISTRATION_EXTRA = "re_registration"; - private SmsRetrieverReceiver smsRetrieverReceiver; + private SmsRetrieverReceiver smsRetrieverReceiver; + private RegistrationViewModel viewModel; - /** - */ public static Intent newIntentForNewRegistration(@NonNull Context context, @Nullable Intent originalIntent) { Intent intent = new Intent(context, RegistrationNavigationActivity.class); intent.putExtra(RE_REGISTRATION_EXTRA, false); @@ -58,6 +59,8 @@ public final class RegistrationNavigationActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + viewModel = ViewModelProviders.of(this, new RegistrationViewModel.Factory(this, isReregister(getIntent()))).get(RegistrationViewModel.class); + setContentView(R.layout.activity_registration_navigation); initializeChallengeListener(); @@ -73,6 +76,8 @@ public final class RegistrationNavigationActivity extends AppCompatActivity { if (intent.getData() != null) { CommunicationActions.handlePotentialProxyLinkUrl(this, intent.getDataString()); } + + viewModel.setIsReregister(isReregister(intent)); } @Override @@ -81,6 +86,10 @@ public final class RegistrationNavigationActivity extends AppCompatActivity { shutdownChallengeListener(); } + private boolean isReregister(@NonNull Intent intent) { + return intent.getBooleanExtra(RE_REGISTRATION_EXTRA, false); + } + private void initializeChallengeListener() { smsRetrieverReceiver = new SmsRetrieverReceiver(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java new file mode 100644 index 0000000000..02bb1727fc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java @@ -0,0 +1,185 @@ +package org.thoughtcrime.securesms.registration; + +import android.app.Application; +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.zkgroup.profiles.ProfileKey; +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; +import org.thoughtcrime.securesms.crypto.PreKeyUtil; +import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; +import org.thoughtcrime.securesms.crypto.SenderKeyUtil; +import org.thoughtcrime.securesms.crypto.SessionUtil; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.IdentityDatabase; +import org.thoughtcrime.securesms.database.RecipientDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; +import org.thoughtcrime.securesms.jobs.RotateCertificateJob; +import org.thoughtcrime.securesms.pin.PinState; +import org.thoughtcrime.securesms.push.AccountManagerFactory; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.registration.VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse; +import org.thoughtcrime.securesms.service.DirectoryRefreshListener; +import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.libsignal.util.KeyHelper; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.KbsPinData; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.ServiceResponse; +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.schedulers.Schedulers; + +/** + * Operations required for finalizing the registration of an account. This is + * to be used after verifying the code and registration lock (if necessary) with + * the server and being issued a UUID. + */ +public final class RegistrationRepository { + + private static final String TAG = Log.tag(RegistrationRepository.class); + + private final Application context; + + public RegistrationRepository(@NonNull Application context) { + this.context = context; + } + + public int getRegistrationId() { + int registrationId = TextSecurePreferences.getLocalRegistrationId(context); + if (registrationId == 0) { + registrationId = KeyHelper.generateRegistrationId(false); + TextSecurePreferences.setLocalRegistrationId(context, registrationId); + } + return registrationId; + } + + public @NonNull ProfileKey getProfileKey(@NonNull String e164) { + ProfileKey profileKey = findExistingProfileKey(context, e164); + + if (profileKey == null) { + profileKey = ProfileKeyUtil.createNew(); + Log.i(TAG, "No profile key found, created a new one"); + } + + return profileKey; + } + + public Single> registerAccountWithoutRegistrationLock(@NonNull RegistrationData registrationData, + @NonNull VerifyAccountResponse response) + { + return registerAccount(registrationData, response, null, null); + } + + public Single> registerAccountWithRegistrationLock(@NonNull RegistrationData registrationData, + @NonNull VerifyAccountWithRegistrationLockResponse response, + @NonNull String pin) + { + return registerAccount(registrationData, response.getVerifyAccountResponse(), pin, response.getKbsData()); + } + + private Single> registerAccount(@NonNull RegistrationData registrationData, + @NonNull VerifyAccountResponse response, + @Nullable String pin, + @Nullable KbsPinData kbsData) + { + return Single.>fromCallable(() -> { + try { + registerAccountInternal(registrationData, response, pin, kbsData); + + JobManager jobManager = ApplicationDependencies.getJobManager(); + jobManager.add(new DirectoryRefreshJob(false)); + jobManager.add(new RotateCertificateJob()); + + DirectoryRefreshListener.schedule(context); + RotateSignedPreKeyListener.schedule(context); + + return ServiceResponse.forResult(response, 200, null); + } catch (IOException e) { + return ServiceResponse.forUnknownError(e); + } + }).subscribeOn(Schedulers.io()); + } + + @WorkerThread + private void registerAccountInternal(@NonNull RegistrationData registrationData, + @NonNull VerifyAccountResponse response, + @Nullable String pin, + @Nullable KbsPinData kbsData) + throws IOException + { + SessionUtil.archiveAllSessions(); + SenderKeyUtil.clearAllState(context); + + UUID uuid = UuidUtil.parseOrThrow(response.getUuid()); + boolean hasPin = response.isStorageCapable(); + + IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context); + List records = PreKeyUtil.generatePreKeys(context); + SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true); + + SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, uuid, registrationData.getE164(), registrationData.getPassword()); + accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records); + + if (registrationData.isFcm()) { + accountManager.setGcmId(Optional.fromNullable(registrationData.getFcmToken())); + } + + RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); + RecipientId selfId = Recipient.externalPush(context, uuid, registrationData.getE164(), true).getId(); + + recipientDatabase.setProfileSharing(selfId, true); + recipientDatabase.markRegisteredOrThrow(selfId, uuid); + + TextSecurePreferences.setLocalNumber(context, registrationData.getE164()); + TextSecurePreferences.setLocalUuid(context, uuid); + recipientDatabase.setProfileKey(selfId, registrationData.getProfileKey()); + ApplicationDependencies.getRecipientCache().clearSelf(); + + TextSecurePreferences.setFcmToken(context, registrationData.getFcmToken()); + TextSecurePreferences.setFcmDisabled(context, registrationData.isNotFcm()); + TextSecurePreferences.setWebsocketRegistered(context, true); + + DatabaseFactory.getIdentityDatabase(context) + .saveIdentity(selfId, + identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED, + true, System.currentTimeMillis(), true); + + TextSecurePreferences.setPushRegistered(context, true); + TextSecurePreferences.setPushServerPassword(context, registrationData.getPassword()); + TextSecurePreferences.setSignedPreKeyRegistered(context, true); + TextSecurePreferences.setPromptedPushRegistration(context, true); + TextSecurePreferences.setUnauthorizedReceived(context, false); + + PinState.onRegistration(context, kbsData, pin, hasPin); + } + + @WorkerThread + private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) { + RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); + Optional recipient = recipientDatabase.getByE164(e164number); + + if (recipient.isPresent()) { + return ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).getProfileKey()); + } + + return null; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RequestVerificationCodeResponseProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/RequestVerificationCodeResponseProcessor.kt new file mode 100644 index 0000000000..4904561ed7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RequestVerificationCodeResponseProcessor.kt @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.registration + +import org.whispersystems.signalservice.api.push.exceptions.LocalRateLimitException +import org.whispersystems.signalservice.internal.ServiceResponse +import org.whispersystems.signalservice.internal.ServiceResponseProcessor +import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse + +/** + * Process responses from requesting an SMS or Phone code from the server. + */ +class RequestVerificationCodeResponseProcessor(response: ServiceResponse) : ServiceResponseProcessor(response) { + public override fun captchaRequired(): Boolean { + return super.captchaRequired() + } + + public override fun rateLimit(): Boolean { + return super.rateLimit() + } + + public override fun getError(): Throwable? { + return super.getError() + } + + fun localRateLimit(): Boolean { + return error is LocalRateLimitException + } + + companion object { + @JvmStatic + fun forLocalRateLimit(): RequestVerificationCodeResponseProcessor { + val response: ServiceResponse = ServiceResponse.forExecutionError(LocalRateLimitException()) + return RequestVerificationCodeResponseProcessor(response) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt new file mode 100644 index 0000000000..404ff7f32f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt @@ -0,0 +1,131 @@ +package org.thoughtcrime.securesms.registration + +import android.app.Application +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.AppCapabilities +import org.thoughtcrime.securesms.gcm.FcmUtil +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.pin.KbsRepository +import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException +import org.thoughtcrime.securesms.pin.TokenData +import org.thoughtcrime.securesms.push.AccountManagerFactory +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.libsignal.util.guava.Optional +import org.whispersystems.signalservice.api.KbsPinData +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException +import org.whispersystems.signalservice.api.SignalServiceAccountManager +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess +import org.whispersystems.signalservice.internal.ServiceResponse +import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse +import java.util.Locale +import java.util.concurrent.TimeUnit + +/** + * Request SMS/Phone verification codes to help prove ownership of a phone number. + */ +class VerifyAccountRepository(private val context: Application) { + + fun requestVerificationCode( + e164: String, + password: String, + mode: Mode, + captchaToken: String? = null + ): Single> { + Log.d(TAG, "SMS Verification requested") + + return Single.fromCallable { + val fcmToken: Optional = FcmUtil.getToken() + val accountManager = AccountManagerFactory.createUnauthenticated(context, e164, password) + val pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, fcmToken, e164, PUSH_REQUEST_TIMEOUT) + + if (mode == Mode.PHONE_CALL) { + accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.fromNullable(captchaToken), pushChallenge, fcmToken) + } else { + accountManager.requestSmsVerificationCode(mode.isSmsRetrieverSupported, Optional.fromNullable(captchaToken), pushChallenge, fcmToken) + } + }.subscribeOn(Schedulers.io()) + } + + fun verifyAccount(registrationData: RegistrationData): Single> { + val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context) + val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey) + + val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated( + context, + registrationData.e164, + registrationData.password + ) + + return Single.fromCallable { + accountManager.verifyAccount( + registrationData.code, + registrationData.registrationId, + registrationData.isNotFcm, + unidentifiedAccessKey, + universalUnidentifiedAccess, + AppCapabilities.getCapabilities(true), + SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable + ) + }.subscribeOn(Schedulers.io()) + } + + fun verifyAccountWithPin(registrationData: RegistrationData, pin: String, tokenData: TokenData): Single> { + val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context) + val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey) + + val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated( + context, + registrationData.e164, + registrationData.password + ) + + return Single.fromCallable { + try { + val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!! + val registrationLockV2: String = kbsData.masterKey.deriveRegistrationLock() + + val response: ServiceResponse = accountManager.verifyAccountWithRegistrationLockPin( + registrationData.code, + registrationData.registrationId, + registrationData.isNotFcm, + registrationLockV2, + unidentifiedAccessKey, + universalUnidentifiedAccess, + AppCapabilities.getCapabilities(true), + SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable + ) + VerifyAccountWithRegistrationLockResponse.from(response, kbsData) + } catch (e: KeyBackupSystemWrongPinException) { + ServiceResponse.forExecutionError(e) + } catch (e: KeyBackupSystemNoDataException) { + ServiceResponse.forExecutionError(e) + } + }.subscribeOn(Schedulers.io()) + } + + enum class Mode(val isSmsRetrieverSupported: Boolean) { + SMS_WITH_LISTENER(true), + SMS_WITHOUT_LISTENER(false), + PHONE_CALL(false); + } + + companion object { + private val TAG = Log.tag(VerifyAccountRepository::class.java) + private val PUSH_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(5) + } + + data class VerifyAccountWithRegistrationLockResponse(val verifyAccountResponse: VerifyAccountResponse, val kbsData: KbsPinData) { + companion object { + fun from(response: ServiceResponse, kbsData: KbsPinData): ServiceResponse { + return if (response.result.isPresent) { + ServiceResponse.forResult(VerifyAccountWithRegistrationLockResponse(response.result.get(), kbsData), 200, null) + } else { + ServiceResponse.coerceError(response) + } + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountResponseProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountResponseProcessor.kt new file mode 100644 index 0000000000..3072e32adb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountResponseProcessor.kt @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.registration + +import org.thoughtcrime.securesms.pin.TokenData +import org.whispersystems.signalservice.internal.ServiceResponse +import org.whispersystems.signalservice.internal.ServiceResponseProcessor +import org.whispersystems.signalservice.internal.push.LockedException +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse + +/** + * Process responses from attempting to verify an account for use in account registration. + */ +sealed class VerifyAccountResponseProcessor(response: ServiceResponse) : ServiceResponseProcessor(response) { + + open val tokenData: TokenData? = null + + public override fun authorizationFailed(): Boolean { + return super.authorizationFailed() + } + + public override fun registrationLock(): Boolean { + return super.registrationLock() + } + + public override fun rateLimit(): Boolean { + return super.rateLimit() + } + + public override fun getError(): Throwable? { + return super.getError() + } + + fun getLockedException(): LockedException { + return error as LockedException + } + + abstract fun isKbsLocked(): Boolean +} + +/** + * Verify processor specific to verifying without needing to handle registration lock. + */ +class VerifyAccountResponseWithoutKbs(response: ServiceResponse) : VerifyAccountResponseProcessor(response) { + override fun isKbsLocked(): Boolean { + return registrationLock() && getLockedException().basicStorageCredentials == null + } +} + +/** + * Verify processor specific to verifying and successfully retrieving KBS information to + * later attempt to verif with registration lock data (pin). + */ +class VerifyAccountResponseWithSuccessfulKbs( + response: ServiceResponse, + override val tokenData: TokenData +) : VerifyAccountResponseProcessor(response) { + + override fun isKbsLocked(): Boolean { + return registrationLock() && tokenData.triesRemaining == 0 + } +} + +/** + * Verify processor specific to verifying and unsuccessfully retrieving KBS information that + * is required for attempting to verify a registration locked account. + */ +class VerifyAccountResponseWithFailedKbs(response: ServiceResponse) : VerifyAccountResponseProcessor(ServiceResponse.coerceError(response)) { + override fun isKbsLocked(): Boolean { + return false + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyCodeWithRegistrationLockResponseProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyCodeWithRegistrationLockResponseProcessor.kt new file mode 100644 index 0000000000..9a1f2204fc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyCodeWithRegistrationLockResponseProcessor.kt @@ -0,0 +1,48 @@ +package org.thoughtcrime.securesms.registration + +import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException +import org.thoughtcrime.securesms.pin.TokenData +import org.thoughtcrime.securesms.registration.VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException +import org.whispersystems.signalservice.internal.ServiceResponse +import org.whispersystems.signalservice.internal.ServiceResponseProcessor +import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse +import org.whispersystems.signalservice.internal.push.VerifyAccountResponse + +/** + * Process responses from attempting to verify an account with registration lock for use in + * account registration. + */ +class VerifyCodeWithRegistrationLockResponseProcessor( + response: ServiceResponse, + val token: TokenData +) : ServiceResponseProcessor(response) { + + public override fun rateLimit(): Boolean { + return super.rateLimit() + } + + public override fun getError(): Throwable? { + return super.getError() + } + + fun wrongPin(): Boolean { + return error is KeyBackupSystemWrongPinException + } + + fun getTokenResponse(): TokenResponse { + return (error as KeyBackupSystemWrongPinException).tokenResponse + } + + fun isKbsLocked(): Boolean { + return error is KeyBackupSystemNoDataException + } + + fun updatedIfRegistrationFailed(response: ServiceResponse): VerifyCodeWithRegistrationLockResponseProcessor { + if (response.result.isPresent) { + return this + } + + return VerifyCodeWithRegistrationLockResponseProcessor(ServiceResponse.coerceError(response), token) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/AccountLockedFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/AccountLockedFragment.java index 3628b13aa5..e7c87ffdb7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/AccountLockedFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/AccountLockedFragment.java @@ -11,12 +11,15 @@ import android.widget.TextView; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProviders; +import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import java.util.concurrent.TimeUnit; -public class AccountLockedFragment extends BaseRegistrationFragment { +public class AccountLockedFragment extends LoggingFragment { @Override public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -29,7 +32,8 @@ public class AccountLockedFragment extends BaseRegistrationFragment { TextView description = view.findViewById(R.id.account_locked_description); - getModel().getLockedTimeRemaining().observe(getViewLifecycleOwner(), + RegistrationViewModel viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class); + viewModel.getLockedTimeRemaining().observe(getViewLifecycleOwner(), t -> description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t))) ); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationFragment.java deleted file mode 100644 index b2cc6dc581..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationFragment.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.thoughtcrime.securesms.registration.fragments; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.text.SpannableStringBuilder; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.SavedStateViewModelFactory; -import androidx.lifecycle.ViewModelProviders; - -import com.dd.CircularProgressButton; - -import org.signal.core.util.TranslationDetection; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.LoggingFragment; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity; -import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; -import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; -import org.thoughtcrime.securesms.util.SpanUtil; - -import java.util.Locale; - -import static org.thoughtcrime.securesms.registration.RegistrationNavigationActivity.RE_REGISTRATION_EXTRA; - -abstract class BaseRegistrationFragment extends LoggingFragment { - - private static final String TAG = Log.tag(BaseRegistrationFragment.class); - - private RegistrationViewModel model; - - @Override - public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - this.model = getRegistrationViewModel(requireActivity()); - } - - protected @NonNull RegistrationViewModel getModel() { - return model; - } - - protected boolean isReregister() { - Activity activity = getActivity(); - - if (activity == null) { - return false; - } - - return activity.getIntent().getBooleanExtra(RE_REGISTRATION_EXTRA, false); - } - - protected static RegistrationViewModel getRegistrationViewModel(@NonNull FragmentActivity activity) { - SavedStateViewModelFactory savedStateViewModelFactory = new SavedStateViewModelFactory(activity.getApplication(), activity); - - return ViewModelProviders.of(activity, savedStateViewModelFactory).get(RegistrationViewModel.class); - } - - protected static void setSpinning(@Nullable CircularProgressButton button) { - if (button != null) { - button.setClickable(false); - button.setIndeterminateProgressMode(true); - button.setProgress(50); - } - } - - protected static void cancelSpinning(@Nullable CircularProgressButton button) { - if (button != null) { - button.setProgress(0); - button.setIndeterminateProgressMode(false); - button.setClickable(true); - } - } - - protected static void hideKeyboard(@NonNull Context context, @NonNull View view) { - InputMethodManager imm = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - - /** - * Sets view up to allow log submitting after multiple taps. - */ - protected static void setDebugLogSubmitMultiTapView(@Nullable View view) { - if (view == null) return; - - view.setOnClickListener(new View.OnClickListener() { - - private static final int DEBUG_TAP_TARGET = 8; - private static final int DEBUG_TAP_ANNOUNCE = 4; - - private int debugTapCounter; - - @Override - public void onClick(View v) { - Context context = v.getContext(); - - debugTapCounter++; - - if (debugTapCounter >= DEBUG_TAP_TARGET) { - context.startActivity(new Intent(context, SubmitDebugLogActivity.class)); - } else if (debugTapCounter >= DEBUG_TAP_ANNOUNCE) { - int remaining = DEBUG_TAP_TARGET - debugTapCounter; - - Toast.makeText(context, context.getResources().getQuantityString(R.plurals.RegistrationActivity_debug_log_hint, remaining, remaining), Toast.LENGTH_SHORT).show(); - } - } - }); - } - - /** - * Presents a prompt for the user to confirm their number as long as it can be shown in one of their device languages. - */ - protected final void showConfirmNumberDialogIfTranslated(@NonNull Context context, - @StringRes int firstMessageLine, - @NonNull String e164number, - @NonNull Runnable onConfirmed, - @NonNull Runnable onEditNumber) - { - TranslationDetection translationDetection = new TranslationDetection(context); - - if (translationDetection.textExistsInUsersLanguage(firstMessageLine) && - translationDetection.textExistsInUsersLanguage(R.string.RegistrationActivity_is_your_phone_number_above_correct) && - translationDetection.textExistsInUsersLanguage(R.string.RegistrationActivity_edit_number)) - { - CharSequence message = new SpannableStringBuilder().append(context.getString(firstMessageLine)) - .append("\n\n") - .append(SpanUtil.bold(PhoneNumberFormatter.prettyPrint(e164number))) - .append("\n\n") - .append(context.getString(R.string.RegistrationActivity_is_your_phone_number_above_correct)); - - Log.i(TAG, "Showing confirm number dialog (" + context.getString(firstMessageLine) + ")"); - new AlertDialog.Builder(context) - .setMessage(message) - .setPositiveButton(android.R.string.ok, - (a, b) -> { - Log.i(TAG, "Number confirmed"); - onConfirmed.run(); - }) - .setNegativeButton(R.string.RegistrationActivity_edit_number, - (a, b) -> { - Log.i(TAG, "User requested edit number from confirm dialog"); - onEditNumber.run(); - }) - .show(); - } else { - Log.i(TAG, "Confirm number dialog not translated in " + Locale.getDefault() + " skipping"); - onConfirmed.run(); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CaptchaFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CaptchaFragment.java index 94164ed289..ac78f750fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CaptchaFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CaptchaFragment.java @@ -10,14 +10,20 @@ import android.webkit.WebViewClient; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProviders; import androidx.navigation.Navigation; +import androidx.navigation.fragment.NavHostFragment; +import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; /** * Fragment that displays a Captcha in a WebView. */ -public final class CaptchaFragment extends BaseRegistrationFragment { +public final class CaptchaFragment extends LoggingFragment { + + private RegistrationViewModel viewModel; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -46,11 +52,12 @@ public final class CaptchaFragment extends BaseRegistrationFragment { }); webView.loadUrl(RegistrationConstants.SIGNAL_CAPTCHA_URL); + + viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class); } private void handleToken(@NonNull String token) { - getModel().onCaptchaResponse(token); - - Navigation.findNavController(requireView()).navigate(CaptchaFragmentDirections.actionCaptchaComplete()); + viewModel.setCaptchaResponse(token); + NavHostFragment.findNavController(this).navigateUp(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ChooseBackupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ChooseBackupFragment.java index ec531adc62..8a97a9077d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ChooseBackupFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ChooseBackupFragment.java @@ -20,20 +20,18 @@ import androidx.core.text.HtmlCompat; import androidx.navigation.Navigation; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.documents.Document; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.BackupUtil; -public class ChooseBackupFragment extends BaseRegistrationFragment { +public class ChooseBackupFragment extends LoggingFragment { private static final String TAG = Log.tag(ChooseBackupFragment.class); private static final short OPEN_FILE_REQUEST_CODE = 3862; - private View chooseBackupButton; - private TextView learnMore; - @Override public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @@ -44,10 +42,10 @@ public class ChooseBackupFragment extends BaseRegistrationFragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button); + View chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button); chooseBackupButton.setOnClickListener(this::onChooseBackupSelected); - learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more); + TextView learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more); learnMore.setText(HtmlCompat.fromHtml(String.format("%s", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0)); learnMore.setMovementMethod(LinkMovementMethod.getInstance()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CountryPickerFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CountryPickerFragment.java index 42bb5e7cb5..628b5dc7b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CountryPickerFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/CountryPickerFragment.java @@ -14,9 +14,11 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.ListFragment; +import androidx.lifecycle.ViewModelProviders; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import androidx.navigation.Navigation; +import androidx.navigation.fragment.NavHostFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.loaders.CountryListLoader; @@ -39,7 +41,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - model = BaseRegistrationFragment.getRegistrationViewModel(requireActivity()); + model = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class); countryFilter = view.findViewById(R.id.country_search); @@ -56,7 +58,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM model.onCountrySelected(countryName, countryCode); - Navigation.findNavController(view).navigate(CountryPickerFragmentDirections.actionCountrySelected()); + NavHostFragment.findNavController(this).navigateUp(); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java index 65615e0141..bbcae90825 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java @@ -1,5 +1,8 @@ package org.thoughtcrime.securesms.registration.fragments; +import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; +import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated; + import android.animation.Animator; import android.os.Bundle; import android.telephony.PhoneStateListener; @@ -12,38 +15,42 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.navigation.NavController; +import androidx.lifecycle.ViewModelProviders; import androidx.navigation.Navigation; +import androidx.navigation.fragment.NavHostFragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.registration.CallMeCountDownView; import org.thoughtcrime.securesms.components.registration.VerificationCodeView; import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard; -import org.thoughtcrime.securesms.pin.PinRestoreRepository; import org.thoughtcrime.securesms.registration.ReceivedSmsEvent; -import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest; -import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest; -import org.thoughtcrime.securesms.registration.service.RegistrationService; +import org.thoughtcrime.securesms.registration.VerifyAccountRepository; import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.SupportEmailUtil; +import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener; import org.thoughtcrime.securesms.util.concurrent.SimpleTask; +import org.whispersystems.signalservice.internal.push.LockedException; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -public final class EnterCodeFragment extends BaseRegistrationFragment - implements SignalStrengthPhoneStateListener.Callback -{ +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.Disposable; + +public final class EnterCodeFragment extends LoggingFragment implements SignalStrengthPhoneStateListener.Callback { private static final String TAG = Log.tag(EnterCodeFragment.class); @@ -57,7 +64,10 @@ public final class EnterCodeFragment extends BaseRegistrationFragment private View serviceWarning; private boolean autoCompleting; - private PhoneStateListener signalStrengthListener; + private PhoneStateListener signalStrengthListener; + private RegistrationViewModel viewModel; + + private final LifecycleDisposable disposables = new LifecycleDisposable(); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -82,7 +92,7 @@ public final class EnterCodeFragment extends BaseRegistrationFragment signalStrengthListener = new SignalStrengthPhoneStateListener(this, this); connectKeyboard(verificationCodeView, keyboard); - hideKeyboard(requireContext(), view); + ViewUtil.hideKeyboard(requireContext(), view); setOnCodeFullyEnteredListener(verificationCodeView); @@ -99,15 +109,16 @@ public final class EnterCodeFragment extends BaseRegistrationFragment noCodeReceivedHelp.setOnClickListener(v -> sendEmailToSupport()); - RegistrationViewModel model = getModel(); - model.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> { + disposables.bindTo(getViewLifecycleOwner().getLifecycle()); + viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class); + viewModel.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> { if (attempts >= 3) { noCodeReceivedHelp.setVisibility(View.VISIBLE); scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, noCodeReceivedHelp.getBottom()), 15000); } }); - model.onStartEnterCode(); + viewModel.onStartEnterCode(); } private void onWrongNumber() { @@ -117,115 +128,112 @@ public final class EnterCodeFragment extends BaseRegistrationFragment private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) { verificationCodeView.setOnCompleteListener(code -> { - RegistrationViewModel model = getModel(); - model.onVerificationCodeEntered(code); callMeCountDown.setVisibility(View.INVISIBLE); wrongNumber.setVisibility(View.INVISIBLE); keyboard.displayProgress(); - RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret()); + Disposable verify = viewModel.verifyCodeAndRegisterAccountWithoutRegistrationLock(code) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(processor -> { + if (processor.hasResult()) { + handleSuccessfulRegistration(); + } else if (processor.rateLimit()) { + handleRateLimited(); + } else if (processor.registrationLock() && !processor.isKbsLocked()) { + LockedException lockedException = processor.getLockedException(); + handleRegistrationLock(lockedException.getTimeRemaining()); + } else if (processor.isKbsLocked()) { + handleKbsAccountLocked(); + } else if (processor.authorizationFailed()) { + handleIncorrectCodeError(); + } else { + Log.w(TAG, "Unable to verify code", processor.getError()); + handleGeneralError(); + } + }); - registrationService.verifyAccount(requireActivity(), model.getFcmToken(), code, null, null, - new CodeVerificationRequest.VerifyCallback() { - - @Override - public void onSuccessfulRegistration() { - SimpleTask.run(() -> { - long startTime = System.currentTimeMillis(); - try { - FeatureFlags.refreshSync(); - Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to get feature flags."); - } catch (IOException e) { - Log.w(TAG, "Failed to refresh flags after " + (System.currentTimeMillis() - startTime) + " ms.", e); - } - return null; - }, none -> { - keyboard.displaySuccess().addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean result) { - handleSuccessfulRegistration(); - } - }); - }); - } - - @Override - public void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining) { - model.setLockedTimeRemaining(timeRemaining); - keyboard.displayLocked().addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean r) { - Navigation.findNavController(requireView()) - .navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, true)); - } - }); - } - - @Override - public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull PinRestoreRepository.TokenData tokenData, @NonNull String kbsStorageCredentials) { - model.setLockedTimeRemaining(timeRemaining); - model.setKeyBackupTokenData(tokenData); - keyboard.displayLocked().addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean r) { - Navigation.findNavController(requireView()) - .navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, false)); - } - }); - } - - @Override - public void onIncorrectKbsRegistrationLockPin(@NonNull PinRestoreRepository.TokenData tokenData) { - throw new AssertionError("Unexpected, user has made no pin guesses"); - } - - @Override - public void onRateLimited() { - keyboard.displayFailure().addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean r) { - new AlertDialog.Builder(requireContext()) - .setTitle(R.string.RegistrationActivity_too_many_attempts) - .setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later) - .setPositiveButton(android.R.string.ok, (dialog, which) -> { - callMeCountDown.setVisibility(View.VISIBLE); - wrongNumber.setVisibility(View.VISIBLE); - verificationCodeView.clear(); - keyboard.displayKeyboard(); - }) - .show(); - } - }); - } - - @Override - public void onKbsAccountLocked(@Nullable Long timeRemaining) { - if (timeRemaining != null) { - model.setLockedTimeRemaining(timeRemaining); - } - Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked()); - } - - @Override - public void onError() { - Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show(); - keyboard.displayFailure().addListener(new AssertedSuccessListener() { - @Override - public void onSuccess(Boolean result) { - callMeCountDown.setVisibility(View.VISIBLE); - wrongNumber.setVisibility(View.VISIBLE); - verificationCodeView.clear(); - keyboard.displayKeyboard(); - } - }); - } - }); + disposables.add(verify); }); } - private void handleSuccessfulRegistration() { - Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration()); + public void handleSuccessfulRegistration() { + SimpleTask.run(() -> { + long startTime = System.currentTimeMillis(); + try { + FeatureFlags.refreshSync(); + Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to get feature flags."); + } catch (IOException e) { + Log.w(TAG, "Failed to refresh flags after " + (System.currentTimeMillis() - startTime) + " ms.", e); + } + return null; + }, none -> { + keyboard.displaySuccess().addListener(new AssertedSuccessListener() { + @Override + public void onSuccess(Boolean result) { + Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration()); + } + }); + }); + } + + public void handleRateLimited() { + keyboard.displayFailure().addListener(new AssertedSuccessListener() { + @Override + public void onSuccess(Boolean r) { + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); + + builder.setTitle(R.string.RegistrationActivity_too_many_attempts) + .setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + callMeCountDown.setVisibility(View.VISIBLE); + wrongNumber.setVisibility(View.VISIBLE); + verificationCodeView.clear(); + keyboard.displayKeyboard(); + }) + .show(); + } + }); + } + + public void handleRegistrationLock(long timeRemaining) { + keyboard.displayLocked().addListener(new AssertedSuccessListener() { + @Override + public void onSuccess(Boolean r) { + Navigation.findNavController(requireView()) + .navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, false)); + } + }); + } + + public void handleKbsAccountLocked() { + Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked()); + } + + public void handleIncorrectCodeError() { + Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show(); + keyboard.displayFailure().addListener(new AssertedSuccessListener() { + @Override + public void onSuccess(Boolean result) { + callMeCountDown.setVisibility(View.VISIBLE); + wrongNumber.setVisibility(View.VISIBLE); + verificationCodeView.clear(); + keyboard.displayKeyboard(); + } + }); + } + + public void handleGeneralError() { + Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show(); + keyboard.displayFailure().addListener(new AssertedSuccessListener() { + @Override + public void onSuccess(Boolean result) { + callMeCountDown.setVisibility(View.VISIBLE); + wrongNumber.setVisibility(View.VISIBLE); + verificationCodeView.clear(); + keyboard.displayKeyboard(); + } + }); } @Override @@ -283,46 +291,28 @@ public final class EnterCodeFragment extends BaseRegistrationFragment private void handlePhoneCallRequest() { showConfirmNumberDialogIfTranslated(requireContext(), R.string.RegistrationActivity_you_will_receive_a_call_to_verify_this_number, - getModel().getNumber().getE164Number(), + viewModel.getNumber().getE164Number(), this::handlePhoneCallRequestAfterConfirm, this::onWrongNumber); } private void handlePhoneCallRequestAfterConfirm() { - RegistrationViewModel model = getModel(); - String captcha = model.getCaptchaToken(); - model.clearCaptchaResponse(); + Disposable request = viewModel.requestVerificationCode(VerifyAccountRepository.Mode.PHONE_CALL) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(processor -> { + if (processor.hasResult()) { + Toast.makeText(requireContext(), R.string.RegistrationActivity_call_requested, Toast.LENGTH_LONG).show(); + } else if (processor.captchaRequired()) { + NavHostFragment.findNavController(this).navigate(EnterCodeFragmentDirections.actionRequestCaptcha()); + } else if (processor.rateLimit()) { + Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show(); + } else { + Log.w(TAG, "Unable to request phone code", processor.getError()); + Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show(); + } + }); - model.onCallRequested(); - - NavController navController = Navigation.findNavController(callMeCountDown); - - RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret()); - - registrationService.requestVerificationCode(requireActivity(), RegistrationCodeRequest.Mode.PHONE_CALL, captcha, - new RegistrationCodeRequest.SmsVerificationCodeCallback() { - - @Override - public void onNeedCaptcha() { - navController.navigate(EnterCodeFragmentDirections.actionRequestCaptcha()); - } - - @Override - public void requestSent(@Nullable String fcmToken) { - model.setFcmToken(fcmToken); - model.markASuccessfulAttempt(); - } - - @Override - public void onRateLimited() { - Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show(); - } - - @Override - public void onError() { - Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show(); - } - }); + disposables.add(request); } private void connectKeyboard(VerificationCodeView verificationCodeView, VerificationPinKeyboard keyboard) { @@ -341,10 +331,9 @@ public final class EnterCodeFragment extends BaseRegistrationFragment public void onResume() { super.onResume(); - RegistrationViewModel model = getModel(); - model.getLiveNumber().observe(getViewLifecycleOwner(), (s) -> header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, s.getFullFormattedNumber()))); + header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, viewModel.getNumber().getFullFormattedNumber())); - model.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> callMeCountDown.startCountDownTo(callAtTime)); + viewModel.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> callMeCountDown.startCountDownTo(callAtTime)); } private void sendEmailToSupport() { @@ -387,8 +376,11 @@ public final class EnterCodeFragment extends BaseRegistrationFragment @Override public void onAnimationEnd(Animator animation) { serviceWarning.setVisibility(View.GONE); } + @Override public void onAnimationStart(Animator animation) {} + @Override public void onAnimationCancel(Animator animation) {} + @Override public void onAnimationRepeat(Animator animation) {} }) .start(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java index 5aa806b8cb..2e6df34c73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterPhoneNumberFragment.java @@ -1,5 +1,10 @@ package org.thoughtcrime.securesms.registration.fragments; +import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; +import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated; +import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning; +import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning; + import android.content.Context; import android.os.Bundle; import android.text.Editable; @@ -22,11 +27,12 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.ViewModelProviders; import androidx.navigation.NavController; import androidx.navigation.Navigation; +import androidx.navigation.fragment.NavHostFragment; import com.dd.CircularProgressButton; import com.google.android.gms.auth.api.phone.SmsRetriever; @@ -34,20 +40,28 @@ import com.google.android.gms.auth.api.phone.SmsRetrieverClient; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.GoogleApiAvailability; import com.google.android.gms.tasks.Task; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.i18n.phonenumbers.AsYouTypeFormatter; import com.google.i18n.phonenumbers.PhoneNumberUtil; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.LabeledEditText; -import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest; -import org.thoughtcrime.securesms.registration.service.RegistrationService; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode; import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState; import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import org.thoughtcrime.securesms.util.Dialogs; +import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.PlayServicesUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.ViewUtil; -public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.Disposable; + +public final class EnterPhoneNumberFragment extends LoggingFragment { private static final String TAG = Log.tag(EnterPhoneNumberFragment.class); @@ -59,6 +73,9 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { private Spinner countrySpinner; private View cancel; private ScrollView scrollView; + private RegistrationViewModel viewModel; + + private final LifecycleDisposable disposables = new LifecycleDisposable(); @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -67,8 +84,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_registration_enter_phone_number, container, false); } @@ -91,21 +107,23 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { register.setOnClickListener(v -> handleRegister(requireContext())); - if (isReregister()) { + disposables.bindTo(getViewLifecycleOwner().getLifecycle()); + viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class); + + if (viewModel.isReregister()) { cancel.setVisibility(View.VISIBLE); cancel.setOnClickListener(v -> Navigation.findNavController(v).navigateUp()); } else { cancel.setVisibility(View.GONE); } - RegistrationViewModel model = getModel(); - NumberViewState number = model.getNumber(); + NumberViewState number = viewModel.getNumber(); initNumber(number); countryCode.getInput().addTextChangedListener(new CountryCodeChangedListener()); - if (model.hasCaptchaToken()) { + if (viewModel.hasCaptchaToken()) { handleRegister(requireContext()); } @@ -145,7 +163,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { numberInput.setImeOptions(EditorInfo.IME_ACTION_DONE); numberInput.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_DONE) { - hideKeyboard(requireContext(), v); + ViewUtil.hideKeyboard(requireContext(), v); handleRegister(requireContext()); return true; } @@ -164,31 +182,32 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { return; } - final NumberViewState number = getModel().getNumber(); + final NumberViewState number = viewModel.getNumber(); final String e164number = number.getE164Number(); if (!number.isValid()) { Dialogs.showAlertDialog(context, - getString(R.string.RegistrationActivity_invalid_number), - String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), e164number)); + getString(R.string.RegistrationActivity_invalid_number), + String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), e164number)); return; } PlayServicesUtil.PlayServicesStatus fcmStatus = PlayServicesUtil.getPlayServicesStatus(context); if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS) { - confirmNumberPrompt(context, e164number, () -> handleRequestVerification(context, e164number, true)); + confirmNumberPrompt(context, e164number, () -> handleRequestVerification(context, true)); } else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) { - confirmNumberPrompt(context, e164number, () -> handlePromptForNoPlayServices(context, e164number)); + confirmNumberPrompt(context, e164number, () -> handlePromptForNoPlayServices(context)); } else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE) { GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0).show(); } else { - Dialogs.showAlertDialog(context, getString(R.string.RegistrationActivity_play_services_error), - getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable)); + Dialogs.showAlertDialog(context, + getString(R.string.RegistrationActivity_play_services_error), + getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable)); } } - private void handleRequestVerification(@NonNull Context context, @NonNull String e164number, boolean fcmSupported) { + private void handleRequestVerification(@NonNull Context context, boolean fcmSupported) { setSpinning(register); disableAllEntries(); @@ -198,16 +217,16 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { task.addOnSuccessListener(none -> { Log.i(TAG, "Successfully registered SMS listener."); - requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_WITH_LISTENER); + requestVerificationCode(Mode.SMS_WITH_LISTENER); }); task.addOnFailureListener(e -> { Log.w(TAG, "Failed to register SMS listener.", e); - requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER); + requestVerificationCode(Mode.SMS_WITHOUT_LISTENER); }); } else { Log.i(TAG, "FCM is not supported, using no SMS listener"); - requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER); + requestVerificationCode(Mode.SMS_WITHOUT_LISTENER); } } @@ -222,77 +241,39 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { countryCode.setEnabled(true); number.setEnabled(true); countrySpinner.setEnabled(true); - if (isReregister()) { + if (viewModel.isReregister()) { cancel.setVisibility(View.VISIBLE); } } - private void requestVerificationCode(String e164number, @NonNull RegistrationCodeRequest.Mode mode) { - RegistrationViewModel model = getModel(); - String captcha = model.getCaptchaToken(); - model.clearCaptchaResponse(); + private void requestVerificationCode(@NonNull Mode mode) { + NavController navController = NavHostFragment.findNavController(this); - NavController navController = Navigation.findNavController(register); + Disposable request = viewModel.requestVerificationCode(mode) + .doOnSubscribe(unused -> TextSecurePreferences.setPushRegistered(ApplicationDependencies.getApplication(), false)) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(processor -> { + if (processor.hasResult()) { + navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode()); + } else if (processor.localRateLimit()) { + Log.i(TAG, "Unable to request sms code due to local rate limit"); + navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode()); + } else if (processor.captchaRequired()) { + Log.i(TAG, "Unable to request sms code due to captcha required"); + navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha()); + } else if (processor.rateLimit()) { + Log.i(TAG, "Unable to request sms code due to rate limit"); + Toast.makeText(register.getContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show(); + } else { + Log.w(TAG, "Unable to request sms code", processor.getError()); + Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show(); + } - if (!model.getRequestLimiter().canRequest(mode, e164number, System.currentTimeMillis())) { - Log.i(TAG, "Local rate limited"); - navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode()); - cancelSpinning(register); - enableAllEntries(); - return; - } + cancelSpinning(register); + enableAllEntries(); + }); - RegistrationService registrationService = RegistrationService.getInstance(e164number, model.getRegistrationSecret()); - - registrationService.requestVerificationCode(requireActivity(), mode, captcha, - new RegistrationCodeRequest.SmsVerificationCodeCallback() { - - @Override - public void onNeedCaptcha() { - if (getContext() == null) { - Log.i(TAG, "Got onNeedCaptcha response, but fragment is no longer attached."); - return; - } - navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha()); - cancelSpinning(register); - enableAllEntries(); - model.getRequestLimiter().onUnsuccessfulRequest(); - model.updateLimiter(); - } - - @Override - public void requestSent(@Nullable String fcmToken) { - if (getContext() == null) { - Log.i(TAG, "Got requestSent response, but fragment is no longer attached."); - return; - } - model.setFcmToken(fcmToken); - model.markASuccessfulAttempt(); - navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode()); - cancelSpinning(register); - enableAllEntries(); - model.getRequestLimiter().onSuccessfulRequest(mode, e164number, System.currentTimeMillis()); - model.updateLimiter(); - } - - @Override - public void onRateLimited() { - Toast.makeText(register.getContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show(); - cancelSpinning(register); - enableAllEntries(); - model.getRequestLimiter().onUnsuccessfulRequest(); - model.updateLimiter(); - } - - @Override - public void onError() { - Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show(); - cancelSpinning(register); - enableAllEntries(); - model.getRequestLimiter().onUnsuccessfulRequest(); - model.updateLimiter(); - } - }); + disposables.add(request); } private void initializeSpinner(Spinner countrySpinner) { @@ -368,10 +349,8 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { number.getInput().setSelection(numberLength, numberLength); } - RegistrationViewModel model = getModel(); - - model.onCountrySelected(null, countryCode); - setCountryDisplay(model.getNumber().getCountryDisplayName()); + viewModel.onCountrySelected(null, countryCode); + setCountryDisplay(viewModel.getNumber().getCountryDisplayName()); } @Override @@ -391,11 +370,9 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { if (number == null) return; - RegistrationViewModel model = getModel(); + viewModel.setNationalNumber(number); - model.setNationalNumber(number); - - setCountryDisplay(model.getNumber().getCountryDisplayName()); + setCountryDisplay(viewModel.getNumber().getCountryDisplayName()); } @Override @@ -449,13 +426,13 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { reformatText(number.getText()); } - private void handlePromptForNoPlayServices(@NonNull Context context, @NonNull String e164number) { - new AlertDialog.Builder(context) - .setTitle(R.string.RegistrationActivity_missing_google_play_services) - .setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services) - .setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> handleRequestVerification(context, e164number, false)) - .setNegativeButton(android.R.string.cancel, null) - .show(); + private void handlePromptForNoPlayServices(@NonNull Context context) { + new MaterialAlertDialogBuilder(context) + .setTitle(R.string.RegistrationActivity_missing_google_play_services) + .setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services) + .setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> handleRequestVerification(context, false)) + .setNegativeButton(android.R.string.cancel, null) + .show(); } protected final void confirmNumberPrompt(@NonNull Context context, @@ -466,7 +443,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment { R.string.RegistrationActivity_a_verification_code_will_be_sent_to, e164number, () -> { - hideKeyboard(context, number.getInput()); + ViewUtil.hideKeyboard(context, number.getInput()); onConfirmed.run(); }, () -> number.focusAndMoveCursorToEndAndOpenKeyboard()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java index 5fc422c5b9..8b3864cd2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java @@ -9,16 +9,19 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProviders; import androidx.navigation.ActivityNavigator; +import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.MainActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.pin.PinRestoreActivity; import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity; +import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; -public final class RegistrationCompleteFragment extends BaseRegistrationFragment { +public final class RegistrationCompleteFragment extends LoggingFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -30,11 +33,12 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - FragmentActivity activity = requireActivity(); + FragmentActivity activity = requireActivity(); + RegistrationViewModel viewModel = ViewModelProviders.of(activity).get(RegistrationViewModel.class); if (SignalStore.storageService().needsAccountRestore()) { activity.startActivity(new Intent(activity, PinRestoreActivity.class)); - } else if (!isReregister()) { + } else if (!viewModel.isReregister()) { final Intent main = MainActivity.clearTop(activity); final Intent profile = EditProfileActivity.getIntentForUserProfile(activity); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java index ae1f5cf26a..19aff24ff7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java @@ -1,5 +1,9 @@ package org.thoughtcrime.securesms.registration.fragments; +import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; +import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning; +import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning; + import android.content.res.Resources; import android.os.Bundle; import android.text.InputType; @@ -14,37 +18,44 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProviders; import androidx.navigation.Navigation; import com.dd.CircularProgressButton; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob; import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.lock.v2.PinKeyboardType; -import org.thoughtcrime.securesms.pin.PinRestoreRepository.TokenData; -import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest; -import org.thoughtcrime.securesms.registration.service.RegistrationService; +import org.thoughtcrime.securesms.pin.TokenData; import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.SupportEmailUtil; +import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import java.io.IOException; import java.util.concurrent.TimeUnit; -public final class RegistrationLockFragment extends BaseRegistrationFragment { +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.disposables.Disposable; + +public final class RegistrationLockFragment extends LoggingFragment { private static final String TAG = Log.tag(RegistrationLockFragment.class); - /** Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1. */ + /** + * Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1. + */ private static final int MINIMUM_PIN_LENGTH = 4; private EditText pinEntry; @@ -54,6 +65,9 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { private TextView keyboardToggle; private long timeRemaining; private boolean isV1RegistrationLock; + private RegistrationViewModel viewModel; + + private final LifecycleDisposable disposables = new LifecycleDisposable(); @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -87,7 +101,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE); pinEntry.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_DONE) { - hideKeyboard(requireContext(), v); + ViewUtil.hideKeyboard(requireContext(), v); handlePinEntry(); return true; } @@ -97,7 +111,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { enableAndFocusPinEntry(); pinButton.setOnClickListener((v) -> { - hideKeyboard(requireContext(), pinEntry); + ViewUtil.hideKeyboard(requireContext(), pinEntry); handlePinEntry(); }); @@ -111,22 +125,25 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther(); keyboardToggle.setText(resolveKeyboardToggleText(keyboardType)); - getModel().getLockedTimeRemaining() - .observe(getViewLifecycleOwner(), t -> timeRemaining = t); + disposables.bindTo(getViewLifecycleOwner().getLifecycle()); + viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class); - TokenData keyBackupCurrentToken = getModel().getKeyBackupCurrentToken(); + viewModel.getLockedTimeRemaining() + .observe(getViewLifecycleOwner(), t -> timeRemaining = t); + + TokenData keyBackupCurrentToken = viewModel.getKeyBackupCurrentToken(); if (keyBackupCurrentToken != null) { int triesRemaining = keyBackupCurrentToken.getTriesRemaining(); if (triesRemaining <= 3) { int daysRemaining = getLockoutDays(timeRemaining); - new AlertDialog.Builder(requireContext()) - .setTitle(R.string.RegistrationLockFragment__not_many_tries_left) - .setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining)) - .setPositiveButton(android.R.string.ok, null) - .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport()) - .show(); + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.RegistrationLockFragment__not_many_tries_left) + .setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining)) + .setPositiveButton(android.R.string.ok, null) + .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport()) + .show(); } if (triesRemaining < 5) { @@ -137,8 +154,8 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { private String getTriesRemainingDialogMessage(int triesRemaining, int daysRemaining) { Resources resources = requireContext().getResources(); - String tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining); - String days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining); + String tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining); + String days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining); return tries + " " + days; } @@ -167,114 +184,94 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { return; } - RegistrationViewModel model = getModel(); - RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret()); - TokenData tokenData = model.getKeyBackupCurrentToken(); - setSpinning(pinButton); - registrationService.verifyAccount(requireActivity(), - model.getFcmToken(), - model.getTextCodeEntered(), - pin, - tokenData, + Disposable verify = viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(pin) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(processor -> { + if (processor.hasResult()) { + handleSuccessfulPinEntry(); + } else if (processor.wrongPin()) { + onIncorrectKbsRegistrationLockPin(processor.getToken()); + } else if (processor.isKbsLocked()) { + onKbsAccountLocked(); + } else if (processor.rateLimit()) { + onRateLimited(); + } else { + Log.w(TAG, "Unable to verify code with registration lock", processor.getError()); + onError(); + } + }); - new CodeVerificationRequest.VerifyCallback() { + disposables.add(verify); + } - @Override - public void onSuccessfulRegistration() { - handleSuccessfulPinEntry(); - } + public void onIncorrectKbsRegistrationLockPin(@NonNull TokenData tokenData) { + cancelSpinning(pinButton); + pinEntry.getText().clear(); + enableAndFocusPinEntry(); - @Override - public void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining) { - getModel().setLockedTimeRemaining(timeRemaining); + viewModel.setKeyBackupTokenData(tokenData); - cancelSpinning(pinButton); - pinEntry.getText().clear(); - enableAndFocusPinEntry(); + int triesRemaining = tokenData.getTriesRemaining(); - errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin); - } + if (triesRemaining == 0) { + Log.w(TAG, "Account locked. User out of attempts on KBS."); + onAccountLocked(); + return; + } - @Override - public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenData kbsTokenData, @NonNull String kbsStorageCredentials) { - throw new AssertionError("Not expected after a pin guess"); - } + if (triesRemaining == 3) { + int daysRemaining = getLockoutDays(timeRemaining); - @Override - public void onIncorrectKbsRegistrationLockPin(@NonNull TokenData tokenData) { - cancelSpinning(pinButton); - pinEntry.getText().clear(); - enableAndFocusPinEntry(); + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.RegistrationLockFragment__incorrect_pin) + .setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining)) + .setPositiveButton(android.R.string.ok, null) + .show(); + } - model.setKeyBackupTokenData(tokenData); + if (triesRemaining > 5) { + errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again); + } else { + errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining)); + forgotPin.setVisibility(View.VISIBLE); + } + } - int triesRemaining = tokenData.getTriesRemaining(); - if (triesRemaining == 0) { - Log.w(TAG, "Account locked. User out of attempts on KBS."); - onAccountLocked(); - return; - } + public void onRateLimited() { + cancelSpinning(pinButton); + enableAndFocusPinEntry(); - if (triesRemaining == 3) { - int daysRemaining = getLockoutDays(timeRemaining); + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.RegistrationActivity_too_many_attempts) + .setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day) + .setPositiveButton(android.R.string.ok, null) + .show(); + } - new AlertDialog.Builder(requireContext()) - .setTitle(R.string.RegistrationLockFragment__incorrect_pin) - .setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining)) - .setPositiveButton(android.R.string.ok, null) - .show(); - } - if (triesRemaining > 5) { - errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again); - } else { - errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining)); - forgotPin.setVisibility(View.VISIBLE); - } - } + public void onKbsAccountLocked() { + onAccountLocked(); + } - @Override - public void onRateLimited() { - cancelSpinning(pinButton); - enableAndFocusPinEntry(); - new AlertDialog.Builder(requireContext()) - .setTitle(R.string.RegistrationActivity_too_many_attempts) - .setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day) - .setPositiveButton(android.R.string.ok, null) - .show(); - } + public void onError() { + cancelSpinning(pinButton); + enableAndFocusPinEntry(); - @Override - public void onKbsAccountLocked(@Nullable Long timeRemaining) { - if (timeRemaining != null) { - model.setLockedTimeRemaining(timeRemaining); - } - - onAccountLocked(); - } - - @Override - public void onError() { - cancelSpinning(pinButton); - enableAndFocusPinEntry(); - - Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show(); - } - }); + Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show(); } private void handleForgottenPin(long timeRemainingMs) { int lockoutDays = getLockoutDays(timeRemainingMs); - new AlertDialog.Builder(requireContext()) - .setTitle(R.string.RegistrationLockFragment__forgot_your_pin) - .setMessage(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays)) - .setPositiveButton(android.R.string.ok, null) - .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport()) - .show(); + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.RegistrationLockFragment__forgot_your_pin) + .setMessage(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays)) + .setPositiveButton(android.R.string.ok, null) + .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport()) + .show(); } private static int getLockoutDays(long timeRemainingMs) { @@ -288,7 +285,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { private void updateKeyboard(@NonNull PinKeyboardType keyboard) { boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC; - pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD + pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD : InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD); pinEntry.getText().clear(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationViewDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationViewDelegate.kt new file mode 100644 index 0000000000..1c8de9f4c3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationViewDelegate.kt @@ -0,0 +1,70 @@ +package org.thoughtcrime.securesms.registration.fragments + +import android.content.Context +import android.content.Intent +import android.text.SpannableStringBuilder +import android.view.View +import android.widget.Toast +import androidx.annotation.StringRes +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.signal.core.util.TranslationDetection +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity +import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter +import org.thoughtcrime.securesms.util.SpanUtil + +object RegistrationViewDelegate { + + @JvmStatic + fun setDebugLogSubmitMultiTapView(view: View?) { + view?.setOnClickListener(object : View.OnClickListener { + private val DEBUG_TAP_TARGET = 8 + private val DEBUG_TAP_ANNOUNCE = 4 + private var debugTapCounter = 0 + + override fun onClick(view: View) { + debugTapCounter++ + + if (debugTapCounter >= DEBUG_TAP_TARGET) { + view.context.startActivity(Intent(view.context, SubmitDebugLogActivity::class.java)) + } else if (debugTapCounter >= DEBUG_TAP_ANNOUNCE) { + val remaining = DEBUG_TAP_TARGET - debugTapCounter + Toast.makeText(view.context, view.context.resources.getQuantityString(R.plurals.RegistrationActivity_debug_log_hint, remaining, remaining), Toast.LENGTH_SHORT).show() + } + } + }) + } + + @JvmStatic + fun showConfirmNumberDialogIfTranslated( + context: Context, + @StringRes firstMessageLine: Int, + e164number: String, + onConfirmed: Runnable, + onEditNumber: Runnable + ) { + val translationDetection = TranslationDetection(context) + + if (translationDetection.textExistsInUsersLanguage( + firstMessageLine, + R.string.RegistrationActivity_is_your_phone_number_above_correct, + R.string.RegistrationActivity_edit_number + ) + ) { + val message: CharSequence = SpannableStringBuilder() + .append(context.getString(firstMessageLine)) + .append("\n\n") + .append(SpanUtil.bold(PhoneNumberFormatter.prettyPrint(e164number))) + .append("\n\n") + .append(context.getString(R.string.RegistrationActivity_is_your_phone_number_above_correct)) + + MaterialAlertDialogBuilder(context) + .setMessage(message) + .setPositiveButton(android.R.string.ok) { _, _ -> onConfirmed.run() } + .setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> onEditNumber.run() } + .show() + } else { + onConfirmed.run() + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java index ef2dc17bc1..20f4fec171 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java @@ -1,5 +1,9 @@ package org.thoughtcrime.securesms.registration.fragments; +import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; +import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning; +import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning; + import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; @@ -27,6 +31,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProviders; import androidx.navigation.Navigation; import com.dd.CircularProgressButton; @@ -38,6 +43,7 @@ import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.AppInitialization; +import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.backup.BackupPassphrase; import org.thoughtcrime.securesms.backup.FullBackupBase; @@ -47,6 +53,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.NoExternalStorageException; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.notifications.NotificationChannels; +import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel; import org.thoughtcrime.securesms.service.LocalBackupListener; import org.thoughtcrime.securesms.util.BackupUtil; import org.thoughtcrime.securesms.util.DateUtils; @@ -57,7 +64,7 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import java.io.IOException; import java.util.Locale; -public final class RestoreBackupFragment extends BaseRegistrationFragment { +public final class RestoreBackupFragment extends LoggingFragment { private static final String TAG = Log.tag(RestoreBackupFragment.class); private static final short OPEN_DOCUMENT_TREE_RESULT_CODE = 13782; @@ -94,7 +101,8 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment { .navigate(RestoreBackupFragmentDirections.actionSkip()); }); - if (isReregister()) { + RegistrationViewModel viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class); + if (viewModel.isReregister()) { Log.i(TAG, "Skipping backup restore during re-register."); Navigation.findNavController(view) .navigate(RestoreBackupFragmentDirections.actionSkipNoReturn()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java index 82ab7c6d41..0d7174e9d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/WelcomeFragment.java @@ -1,5 +1,9 @@ package org.thoughtcrime.securesms.registration.fragments; +import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView; +import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning; +import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning; + import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; @@ -16,8 +20,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; -import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.ViewModelProviders; import androidx.navigation.ActivityNavigator; import androidx.navigation.Navigation; import androidx.navigation.fragment.NavHostFragment; @@ -30,6 +34,7 @@ import org.greenrobot.eventbus.EventBus; import org.signal.core.util.logging.Log; import org.signal.devicetransfer.DeviceToDeviceTransferService; import org.signal.devicetransfer.TransferStatus; +import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.permissions.Permissions; @@ -40,52 +45,49 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; -public final class WelcomeFragment extends BaseRegistrationFragment { +public final class WelcomeFragment extends LoggingFragment { private static final String TAG = Log.tag(WelcomeFragment.class); - private static final String[] PERMISSIONS = { Manifest.permission.WRITE_CONTACTS, - Manifest.permission.READ_CONTACTS, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.READ_PHONE_STATE }; + private static final String[] PERMISSIONS = { Manifest.permission.WRITE_CONTACTS, + Manifest.permission.READ_CONTACTS, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.READ_PHONE_STATE }; @RequiresApi(26) - private static final String[] PERMISSIONS_API_26 = { Manifest.permission.WRITE_CONTACTS, - Manifest.permission.READ_CONTACTS, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.READ_PHONE_STATE, - Manifest.permission.READ_PHONE_NUMBERS }; + private static final String[] PERMISSIONS_API_26 = { Manifest.permission.WRITE_CONTACTS, + Manifest.permission.READ_CONTACTS, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.READ_PHONE_STATE, + Manifest.permission.READ_PHONE_NUMBERS }; @RequiresApi(26) - private static final String[] PERMISSIONS_API_29 = { Manifest.permission.WRITE_CONTACTS, - Manifest.permission.READ_CONTACTS, - Manifest.permission.READ_PHONE_STATE, - Manifest.permission.READ_PHONE_NUMBERS }; - private static final @StringRes int RATIONALE = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends; - private static final @StringRes int RATIONALE_API_29 = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends; - private static final int[] HEADERS = { R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp }; - private static final int[] HEADERS_API_29 = { R.drawable.ic_contacts_white_48dp }; + private static final String[] PERMISSIONS_API_29 = { Manifest.permission.WRITE_CONTACTS, + Manifest.permission.READ_CONTACTS, + Manifest.permission.READ_PHONE_STATE, + Manifest.permission.READ_PHONE_NUMBERS }; + + private static final @StringRes int RATIONALE = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends; + private static final @StringRes int RATIONALE_API_29 = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends; + private static final int[] HEADERS = { R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp }; + private static final int[] HEADERS_API_29 = { R.drawable.ic_contacts_white_48dp }; private CircularProgressButton continueButton; - private Button restoreFromBackup; + private RegistrationViewModel viewModel; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(isReregister() ? R.layout.fragment_registration_blank - : R.layout.fragment_registration_welcome, - container, - false); + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_registration_welcome, container, false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - if (isReregister()) { - RegistrationViewModel model = getModel(); + viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class); - if (model.hasRestoreFlowBeenShown()) { + if (viewModel.isReregister()) { + if (viewModel.hasRestoreFlowBeenShown()) { Log.i(TAG, "We've come back to the home fragment on a restore, user must be backing out"); if (!Navigation.findNavController(view).popBackStack()) { FragmentActivity activity = requireActivity(); @@ -98,10 +100,9 @@ public final class WelcomeFragment extends BaseRegistrationFragment { initializeNumber(); Log.i(TAG, "Skipping restore because this is a reregistration."); - model.setWelcomeSkippedOnRestore(); + viewModel.setWelcomeSkippedOnRestore(); Navigation.findNavController(view) .navigate(WelcomeFragmentDirections.actionSkipRestore()); - } else { setDebugLogSubmitMultiTapView(view.findViewById(R.id.image)); @@ -110,7 +111,7 @@ public final class WelcomeFragment extends BaseRegistrationFragment { continueButton = view.findViewById(R.id.welcome_continue_button); continueButton.setOnClickListener(this::continueClicked); - restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore); + Button restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore); restoreFromBackup.setOnClickListener(this::restoreFromBackupClicked); TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button); @@ -211,13 +212,13 @@ public final class WelcomeFragment extends BaseRegistrationFragment { Phonenumber.PhoneNumber phoneNumber = localNumber.get(); String nationalNumber = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL); - getModel().onNumberDetected(phoneNumber.getCountryCode(), nationalNumber); + viewModel.onNumberDetected(phoneNumber.getCountryCode(), nationalNumber); } else { Log.i(TAG, "No number detected"); Optional simCountryIso = Util.getSimCountryIso(requireContext()); if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) { - getModel().onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), ""); + viewModel.onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), ""); } } } @@ -228,7 +229,7 @@ public final class WelcomeFragment extends BaseRegistrationFragment { private boolean canUserSelectBackup() { return BackupUtil.isUserSelectionRequired(requireContext()) && - !isReregister() && + !viewModel.isReregister() && !SignalStore.settings().isBackupEnabled(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java deleted file mode 100644 index 2d40ec9918..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java +++ /dev/null @@ -1,316 +0,0 @@ -package org.thoughtcrime.securesms.registration.service; - -import android.content.Context; -import android.os.AsyncTask; -import android.text.TextUtils; - -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.zkgroup.profiles.ProfileKey; -import org.thoughtcrime.securesms.AppCapabilities; -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; -import org.thoughtcrime.securesms.crypto.PreKeyUtil; -import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; -import org.thoughtcrime.securesms.crypto.SenderKeyUtil; -import org.thoughtcrime.securesms.crypto.SessionUtil; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.IdentityDatabase; -import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobmanager.JobManager; -import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; -import org.thoughtcrime.securesms.jobs.RotateCertificateJob; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.pin.PinRestoreRepository; -import org.thoughtcrime.securesms.pin.PinRestoreRepository.TokenData; -import org.thoughtcrime.securesms.pin.PinState; -import org.thoughtcrime.securesms.push.AccountManagerFactory; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.service.DirectoryRefreshListener; -import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; -import org.whispersystems.libsignal.util.KeyHelper; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.KbsPinData; -import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; -import org.whispersystems.signalservice.api.push.exceptions.RateLimitException; -import org.whispersystems.signalservice.api.util.UuidUtil; -import org.whispersystems.signalservice.internal.push.LockedException; -import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; - -import java.io.IOException; -import java.util.List; -import java.util.UUID; - -public final class CodeVerificationRequest { - - private static final String TAG = Log.tag(CodeVerificationRequest.class); - - private enum Result { - SUCCESS, - PIN_LOCKED, - KBS_WRONG_PIN, - RATE_LIMITED, - KBS_ACCOUNT_LOCKED, - ERROR - } - - /** - * Asynchronously verify the account via the code. - * - * @param fcmToken The FCM token for the device. - * @param code The code that was delivered to the user. - * @param pin The users registration pin. - * @param callback Exactly one method on this callback will be called. - * @param kbsTokenData By keeping the token, on failure, a newly returned token will be reused in subsequent pin - * attempts, preventing certain attacks, we can also track the attempts making missing replies easier to spot. - */ - static void verifyAccount(@NonNull Context context, - @NonNull Credentials credentials, - @Nullable String fcmToken, - @NonNull String code, - @Nullable String pin, - @Nullable TokenData kbsTokenData, - @NonNull VerifyCallback callback) - { - new AsyncTask() { - - private volatile LockedException lockedException; - private volatile TokenData tokenData; - - @Override - protected Result doInBackground(Void... voids) { - final boolean pinSupplied = pin != null; - final boolean tryKbs = tokenData != null; - - try { - this.tokenData = kbsTokenData; - verifyAccount(context, credentials, code, pin, tokenData, fcmToken); - return Result.SUCCESS; - } catch (KeyBackupSystemNoDataException e) { - Log.w(TAG, "No data found on KBS"); - return Result.KBS_ACCOUNT_LOCKED; - } catch (KeyBackupSystemWrongPinException e) { - tokenData = TokenData.withResponse(tokenData, e.getTokenResponse()); - return Result.KBS_WRONG_PIN; - } catch (LockedException e) { - if (pinSupplied && tryKbs) { - throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); - } - - Log.w(TAG, e); - lockedException = e; - if (e.getBasicStorageCredentials() != null) { - try { - tokenData = getToken(e.getBasicStorageCredentials()); - if (tokenData == null || tokenData.getTriesRemaining() == 0) { - return Result.KBS_ACCOUNT_LOCKED; - } - } catch (IOException ex) { - Log.w(TAG, e); - return Result.ERROR; - } - } - return Result.PIN_LOCKED; - } catch (RateLimitException e) { - Log.w(TAG, e); - return Result.RATE_LIMITED; - } catch (IOException e) { - Log.w(TAG, e); - return Result.ERROR; - } - } - - @Override - protected void onPostExecute(Result result) { - switch (result) { - case SUCCESS: - handleSuccessfulRegistration(context); - callback.onSuccessfulRegistration(); - break; - case PIN_LOCKED: - if (tokenData != null) { - if (lockedException.getBasicStorageCredentials() == null) { - throw new AssertionError("KBS Token set, but no storage credentials supplied."); - } - Log.w(TAG, "Reg Locked: V2 pin needed for registration"); - callback.onKbsRegistrationLockPinRequired(lockedException.getTimeRemaining(), tokenData, lockedException.getBasicStorageCredentials()); - } else { - Log.w(TAG, "Reg Locked: V1 pin needed for registration"); - callback.onV1RegistrationLockPinRequiredOrIncorrect(lockedException.getTimeRemaining()); - } - break; - case RATE_LIMITED: - callback.onRateLimited(); - break; - case ERROR: - callback.onError(); - break; - case KBS_WRONG_PIN: - Log.w(TAG, "KBS Pin was wrong"); - callback.onIncorrectKbsRegistrationLockPin(tokenData); - break; - case KBS_ACCOUNT_LOCKED: - Log.w(TAG, "KBS Account is locked"); - callback.onKbsAccountLocked(lockedException != null ? lockedException.getTimeRemaining() : null); - break; - } - } - }.executeOnExecutor(SignalExecutors.UNBOUNDED); - } - - private static TokenData getToken(@Nullable String basicStorageCredentials) throws IOException { - if (basicStorageCredentials == null) return null; - return new PinRestoreRepository().getTokenSync(basicStorageCredentials); - } - - private static void handleSuccessfulRegistration(@NonNull Context context) { - JobManager jobManager = ApplicationDependencies.getJobManager(); - jobManager.add(new DirectoryRefreshJob(false)); - jobManager.add(new RotateCertificateJob()); - - DirectoryRefreshListener.schedule(context); - RotateSignedPreKeyListener.schedule(context); - } - - private static void verifyAccount(@NonNull Context context, - @NonNull Credentials credentials, - @NonNull String code, - @Nullable String pin, - @Nullable TokenData kbsTokenData, - @Nullable String fcmToken) - throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException - { - boolean isV2RegistrationLock = kbsTokenData != null; - int registrationId = KeyHelper.generateRegistrationId(false); - boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context); - ProfileKey profileKey = findExistingProfileKey(context, credentials.getE164number()); - - if (profileKey == null) { - profileKey = ProfileKeyUtil.createNew(); - Log.i(TAG, "No profile key found, created a new one"); - } - - byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(profileKey); - - TextSecurePreferences.setLocalRegistrationId(context, registrationId); - SessionUtil.archiveAllSessions(); - SenderKeyUtil.clearAllState(context); - - SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword()); - KbsPinData kbsData = isV2RegistrationLock ? PinState.restoreMasterKey(pin, kbsTokenData.getEnclave(), kbsTokenData.getBasicAuth(), kbsTokenData.getTokenResponse()) : null; - String registrationLockV2 = kbsData != null ? kbsData.getMasterKey().deriveRegistrationLock() : null; - String registrationLockV1 = isV2RegistrationLock ? null : pin; - boolean hasFcm = fcmToken != null; - - Log.i(TAG, "Calling verifyAccountWithCode(): reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2)); - - VerifyAccountResponse response = accountManager.verifyAccountWithCode(code, - null, - registrationId, - !hasFcm, - registrationLockV1, - registrationLockV2, - unidentifiedAccessKey, - universalUnidentifiedAccess, - AppCapabilities.getCapabilities(true), - SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isDiscoverable()); - - UUID uuid = UuidUtil.parseOrThrow(response.getUuid()); - boolean hasPin = response.isStorageCapable(); - - IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context); - List records = PreKeyUtil.generatePreKeys(context); - SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true); - - accountManager = AccountManagerFactory.createAuthenticated(context, uuid, credentials.getE164number(), credentials.getPassword()); - accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records); - - if (hasFcm) { - accountManager.setGcmId(Optional.fromNullable(fcmToken)); - } - - RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); - RecipientId selfId = Recipient.externalPush(context, uuid, credentials.getE164number(), true).getId(); - - recipientDatabase.setProfileSharing(selfId, true); - recipientDatabase.markRegisteredOrThrow(selfId, uuid); - - TextSecurePreferences.setLocalNumber(context, credentials.getE164number()); - TextSecurePreferences.setLocalUuid(context, uuid); - recipientDatabase.setProfileKey(selfId, profileKey); - ApplicationDependencies.getRecipientCache().clearSelf(); - - TextSecurePreferences.setFcmToken(context, fcmToken); - TextSecurePreferences.setFcmDisabled(context, !hasFcm); - TextSecurePreferences.setWebsocketRegistered(context, true); - - DatabaseFactory.getIdentityDatabase(context) - .saveIdentity(selfId, - identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED, - true, System.currentTimeMillis(), true); - - TextSecurePreferences.setPushRegistered(context, true); - TextSecurePreferences.setPushServerPassword(context, credentials.getPassword()); - TextSecurePreferences.setSignedPreKeyRegistered(context, true); - TextSecurePreferences.setPromptedPushRegistration(context, true); - TextSecurePreferences.setUnauthorizedReceived(context, false); - - PinState.onRegistration(context, kbsData, pin, hasPin); - } - - private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) { - RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); - Optional recipient = recipientDatabase.getByE164(e164number); - - if (recipient.isPresent()) { - return ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).getProfileKey()); - } - - return null; - } - - public interface VerifyCallback { - - void onSuccessfulRegistration(); - - /** - * The account is locked with a V1 (non-KBS) pin. - * - * @param timeRemaining Time until pin expires and number can be reused. - */ - void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining); - - /** - * The account is locked with a V2 (KBS) pin. Called before any user pin guesses. - */ - void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenData kbsTokenData, @NonNull String kbsStorageCredentials); - - /** - * The account is locked with a V2 (KBS) pin. Called after a user pin guess. - *

- * i.e. an attempt has likely been used. - */ - void onIncorrectKbsRegistrationLockPin(@NonNull TokenData kbsTokenResponse); - - /** - * V2 (KBS) pin is set, but there is no data on KBS. - * - * @param timeRemaining Non-null if known. - */ - void onKbsAccountLocked(@Nullable Long timeRemaining); - - void onRateLimited(); - - void onError(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/Credentials.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/Credentials.java deleted file mode 100644 index 4501816bdb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/Credentials.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.thoughtcrime.securesms.registration.service; - -import androidx.annotation.NonNull; - -public final class Credentials { - - private final String e164number; - private final String password; - - public Credentials(@NonNull String e164number, @NonNull String password) { - this.e164number = e164number; - this.password = password; - } - - public @NonNull String getE164number() { - return e164number; - } - - public @NonNull String getPassword() { - return password; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationCodeRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationCodeRequest.java deleted file mode 100644 index ae01e7ca43..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationCodeRequest.java +++ /dev/null @@ -1,146 +0,0 @@ -package org.thoughtcrime.securesms.registration.service; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.AsyncTask; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.gcm.FcmUtil; -import org.thoughtcrime.securesms.push.AccountManagerFactory; -import org.thoughtcrime.securesms.registration.PushChallengeRequest; -import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; -import org.whispersystems.signalservice.api.push.exceptions.RateLimitException; - -import java.io.IOException; -import java.util.Locale; - -public final class RegistrationCodeRequest { - - private static final long PUSH_REQUEST_TIMEOUT_MS = 5000L; - - private static final String TAG = Log.tag(RegistrationCodeRequest.class); - - /** - * Request a verification code to be sent according to the specified {@param mode}. - * - * The request will fire asynchronously, and exactly one of the methods on the {@param callback} - * will be called. - */ - @SuppressLint("StaticFieldLeak") - static void requestSmsVerificationCode(@NonNull Context context, @NonNull Credentials credentials, @Nullable String captchaToken, @NonNull Mode mode, @NonNull SmsVerificationCodeCallback callback) { - Log.d(TAG, "SMS Verification requested"); - - new AsyncTask() { - @Override - protected @NonNull - VerificationRequestResult doInBackground(Void... voids) { - try { - markAsVerifying(context); - - Optional fcmToken = FcmUtil.getToken(); - - SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword()); - - Optional pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, fcmToken, credentials.getE164number(), PUSH_REQUEST_TIMEOUT_MS); - - if (mode == Mode.PHONE_CALL) { - accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.fromNullable(captchaToken), pushChallenge); - } else { - accountManager.requestSmsVerificationCode(mode.isSmsRetrieverSupported(), Optional.fromNullable(captchaToken), pushChallenge); - } - - return new VerificationRequestResult(fcmToken.orNull(), Optional.absent()); - } catch (IOException e) { - org.signal.core.util.logging.Log.w(TAG, "Error during account registration", e); - return new VerificationRequestResult(null, Optional.of(e)); - } - } - - protected void onPostExecute(@NonNull VerificationRequestResult result) { - if (isCaptchaRequired(result)) { - callback.onNeedCaptcha(); - } else if (isRateLimited(result)) { - callback.onRateLimited(); - } else if (result.exception.isPresent()) { - callback.onError(); - } else { - callback.requestSent(result.fcmToken); - } - } - }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private static void markAsVerifying(Context context) { - TextSecurePreferences.setPushRegistered(context, false); - } - - private static boolean isCaptchaRequired(@NonNull VerificationRequestResult result) { - return result.exception.isPresent() && result.exception.get() instanceof CaptchaRequiredException; - } - - private static boolean isRateLimited(@NonNull VerificationRequestResult result) { - return result.exception.isPresent() && result.exception.get() instanceof RateLimitException; - } - - private static class VerificationRequestResult { - private final @Nullable String fcmToken; - private final Optional exception; - - private VerificationRequestResult(@Nullable String fcmToken, Optional exception) { - this.fcmToken = fcmToken; - this.exception = exception; - } - } - - /** - * The mode by which a code is being requested. - */ - public enum Mode { - - /** - * Device is requesting an SMS and supports SMS retrieval. - * - * The SMS sent will be formatted for automatic SMS retrieval. - */ - SMS_WITH_LISTENER(true), - - /** - * Device is requesting an SMS and does not support SMS retrieval. - * - * The SMS sent will be not be specially formatted for automatic SMS retrieval. - */ - SMS_WITHOUT_LISTENER(false), - - /** - * Device is requesting a phone call. - */ - PHONE_CALL(false); - - private final boolean smsRetrieverSupported; - - Mode(boolean smsRetrieverSupported) { - this.smsRetrieverSupported = smsRetrieverSupported; - } - - public boolean isSmsRetrieverSupported() { - return smsRetrieverSupported; - } - } - - public interface SmsVerificationCodeCallback { - - void onNeedCaptcha(); - - void requestSent(@Nullable String fcmToken); - - void onRateLimited(); - - void onError(); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationService.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationService.java deleted file mode 100644 index efba5883a7..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationService.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.thoughtcrime.securesms.registration.service; - -import android.app.Activity; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.thoughtcrime.securesms.pin.PinRestoreRepository; - -public final class RegistrationService { - - private final Credentials credentials; - - private RegistrationService(@NonNull Credentials credentials) { - this.credentials = credentials; - } - - public static RegistrationService getInstance(@NonNull String e164number, @NonNull String password) { - return new RegistrationService(new Credentials(e164number, password)); - } - - /** - * See {@link RegistrationCodeRequest}. - */ - public void requestVerificationCode(@NonNull Activity activity, - @NonNull RegistrationCodeRequest.Mode mode, - @Nullable String captchaToken, - @NonNull RegistrationCodeRequest.SmsVerificationCodeCallback callback) - { - RegistrationCodeRequest.requestSmsVerificationCode(activity, credentials, captchaToken, mode, callback); - } - - /** - * See {@link CodeVerificationRequest}. - */ - public void verifyAccount(@NonNull Activity activity, - @Nullable String fcmToken, - @NonNull String code, - @Nullable String pin, - @Nullable PinRestoreRepository.TokenData tokenData, - @NonNull CodeVerificationRequest.VerifyCallback callback) - { - CodeVerificationRequest.verifyAccount(activity, credentials, fcmToken, code, pin, tokenData, callback); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiter.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiter.java index c6b233b821..4e8780f22d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiter.java @@ -6,7 +6,7 @@ import android.os.Parcelable; import androidx.annotation.MainThread; import androidx.annotation.NonNull; -import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest; +import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode; import java.util.HashMap; import java.util.Map; @@ -14,8 +14,8 @@ import java.util.Objects; public final class LocalCodeRequestRateLimiter implements Parcelable { - private final long timePeriod; - private final Map dataMap; + private final long timePeriod; + private final Map dataMap; public LocalCodeRequestRateLimiter(long timePeriod) { this.timePeriod = timePeriod; @@ -23,7 +23,7 @@ public final class LocalCodeRequestRateLimiter implements Parcelable { } @MainThread - public boolean canRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) { + public boolean canRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) { Data data = dataMap.get(mode); return data == null || !data.limited(e164Number, currentTime); @@ -33,7 +33,7 @@ public final class LocalCodeRequestRateLimiter implements Parcelable { * Call this when the server has returned that it was successful in requesting a code via the specified mode. */ @MainThread - public void onSuccessfulRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) { + public void onSuccessfulRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) { dataMap.put(mode, new Data(e164Number, currentTime + timePeriod)); } @@ -69,9 +69,9 @@ public final class LocalCodeRequestRateLimiter implements Parcelable { LocalCodeRequestRateLimiter localCodeRequestRateLimiter = new LocalCodeRequestRateLimiter(timePeriod); for (int i = 0; i < numberOfMapEntries; i++) { - RegistrationCodeRequest.Mode mode = RegistrationCodeRequest.Mode.values()[in.readInt()]; - String e164Number = in.readString(); - long limitedUntil = in.readLong(); + Mode mode = Mode.values()[in.readInt()]; + String e164Number = in.readString(); + long limitedUntil = in.readLong(); localCodeRequestRateLimiter.dataMap.put(mode, new Data(Objects.requireNonNull(e164Number), limitedUntil)); } @@ -94,7 +94,7 @@ public final class LocalCodeRequestRateLimiter implements Parcelable { dest.writeLong(timePeriod); dest.writeInt(dataMap.size()); - for (Map.Entry a : dataMap.entrySet()) { + for (Map.Entry a : dataMap.entrySet()) { dest.writeInt(a.getKey().ordinal()); dest.writeString(a.getValue().e164Number); dest.writeLong(a.getValue().limitedUntil); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java index ccaec3a340..6cefc2bb45 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java @@ -3,90 +3,117 @@ package org.thoughtcrime.securesms.registration.viewmodel; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.lifecycle.AbstractSavedStateViewModelFactory; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.SavedStateHandle; import androidx.lifecycle.ViewModel; +import androidx.savedstate.SavedStateRegistryOwner; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.pin.PinRestoreRepository.TokenData; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.pin.KbsRepository; +import org.thoughtcrime.securesms.pin.TokenData; +import org.thoughtcrime.securesms.registration.RegistrationData; +import org.thoughtcrime.securesms.registration.RegistrationRepository; +import org.thoughtcrime.securesms.registration.RequestVerificationCodeResponseProcessor; +import org.thoughtcrime.securesms.registration.VerifyAccountRepository; +import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode; +import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor; +import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithFailedKbs; +import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithSuccessfulKbs; +import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs; +import org.thoughtcrime.securesms.registration.VerifyCodeWithRegistrationLockResponseProcessor; import org.thoughtcrime.securesms.util.Util; +import java.util.Objects; import java.util.concurrent.TimeUnit; -public final class RegistrationViewModel extends ViewModel { +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; - private static final String TAG = Log.tag(RegistrationViewModel.class); +public final class RegistrationViewModel extends ViewModel { private static final long FIRST_CALL_AVAILABLE_AFTER_MS = TimeUnit.SECONDS.toMillis(64); private static final long SUBSEQUENT_CALL_AVAILABLE_AFTER_MS = TimeUnit.SECONDS.toMillis(300); - private final String secret; - private final MutableLiveData number; - private final MutableLiveData textCodeEntered; - private final MutableLiveData captchaToken; - private final MutableLiveData fcmToken; - private final MutableLiveData restoreFlowShown; - private final MutableLiveData successfulCodeRequestAttempts; - private final MutableLiveData requestLimiter; - private final MutableLiveData kbsTokenData; - private final MutableLiveData lockedTimeRemaining; - private final MutableLiveData canCallAtTime; + private static final String STATE_REGISTRATION_SECRET = "REGISTRATION_SECRET"; + private static final String STATE_NUMBER = "NUMBER"; + private static final String STATE_VERIFICATION_CODE = "TEXT_CODE_ENTERED"; + private static final String STATE_CAPTCHA = "CAPTCHA"; + private static final String STATE_FCM_TOKEN = "FCM_TOKEN"; + private static final String STATE_RESTORE_FLOW_SHOWN = "RESTORE_FLOW_SHOWN"; + private static final String STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS = "SUCCESSFUL_CODE_REQUEST_ATTEMPTS"; + private static final String STATE_REQUEST_RATE_LIMITER = "REQUEST_RATE_LIMITER"; + private static final String STATE_KBS_TOKEN = "KBS_TOKEN"; + private static final String STATE_TIME_REMAINING = "TIME_REMAINING"; + private static final String STATE_CAN_CALL_AT_TIME = "CAN_CALL_AT_TIME"; + private static final String STATE_IS_REREGISTER = "IS_REREGISTER"; - public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) { - secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18)); + private final SavedStateHandle registrationState; + private final VerifyAccountRepository verifyAccountRepository; + private final KbsRepository kbsRepository; + private final RegistrationRepository registrationRepository; - number = savedStateHandle.getLiveData("NUMBER", NumberViewState.INITIAL); - textCodeEntered = savedStateHandle.getLiveData("TEXT_CODE_ENTERED", ""); - captchaToken = savedStateHandle.getLiveData("CAPTCHA"); - fcmToken = savedStateHandle.getLiveData("FCM_TOKEN"); - restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false); - successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0); - requestLimiter = savedStateHandle.getLiveData("REQUEST_RATE_LIMITER", new LocalCodeRequestRateLimiter(60_000)); - kbsTokenData = savedStateHandle.getLiveData("KBS_TOKEN"); - lockedTimeRemaining = savedStateHandle.getLiveData("TIME_REMAINING", 0L); - canCallAtTime = savedStateHandle.getLiveData("CAN_CALL_AT_TIME", 0L); + public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle, + boolean isReregister, + @NonNull VerifyAccountRepository verifyAccountRepository, + @NonNull KbsRepository kbsRepository, + @NonNull RegistrationRepository registrationRepository) + { + this.registrationState = savedStateHandle; + this.verifyAccountRepository = verifyAccountRepository; + this.kbsRepository = kbsRepository; + this.registrationRepository = registrationRepository; + + setInitialDefaultValue(this.registrationState, STATE_REGISTRATION_SECRET, Util.getSecret(18)); + setInitialDefaultValue(this.registrationState, STATE_NUMBER, NumberViewState.INITIAL); + setInitialDefaultValue(this.registrationState, STATE_VERIFICATION_CODE, ""); + setInitialDefaultValue(this.registrationState, STATE_RESTORE_FLOW_SHOWN, false); + setInitialDefaultValue(this.registrationState, STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0); + setInitialDefaultValue(this.registrationState, STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000)); + + this.registrationState.set(STATE_IS_REREGISTER, isReregister); } - private static T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) { - if (!savedStateHandle.contains(key)) { + private static void setInitialDefaultValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) { + if (!savedStateHandle.contains(key) || savedStateHandle.get(key) == null) { savedStateHandle.set(key, initialValue); } - return savedStateHandle.get(key); + } + + public boolean isReregister() { + //noinspection ConstantConditions + return registrationState.get(STATE_IS_REREGISTER); } public @NonNull NumberViewState getNumber() { - //noinspection ConstantConditions Live data was given an initial value - return number.getValue(); - } - - public @NonNull LiveData getLiveNumber() { - return number; + //noinspection ConstantConditions + return registrationState.get(STATE_NUMBER); } public @NonNull String getTextCodeEntered() { - //noinspection ConstantConditions Live data was given an initial value - return textCodeEntered.getValue(); + //noinspection ConstantConditions + return registrationState.get(STATE_VERIFICATION_CODE); } - public String getCaptchaToken() { - return captchaToken.getValue(); + private @Nullable String getCaptchaToken() { + return registrationState.get(STATE_CAPTCHA); } public boolean hasCaptchaToken() { return getCaptchaToken() != null; } - public String getRegistrationSecret() { - return secret; + private @NonNull String getRegistrationSecret() { + //noinspection ConstantConditions + return registrationState.get(STATE_REGISTRATION_SECRET); } - public void onCaptchaResponse(String captchaToken) { - this.captchaToken.setValue(captchaToken); + public void setCaptchaResponse(@Nullable String captchaToken) { + registrationState.set(STATE_CAPTCHA, captchaToken); } - public void clearCaptchaResponse() { - captchaToken.setValue(null); + private void clearCaptchaResponse() { + setCaptchaResponse(null); } public void onCountrySelected(@Nullable String selectedCountryName, int countryCode) { @@ -102,13 +129,13 @@ public final class RegistrationViewModel extends ViewModel { private void setViewState(NumberViewState numberViewState) { if (!numberViewState.equals(getNumber())) { - number.setValue(numberViewState); + registrationState.set(STATE_NUMBER, numberViewState); } } @MainThread public void onVerificationCodeEntered(String code) { - textCodeEntered.setValue(code); + registrationState.set(STATE_VERIFICATION_CODE, code); } public void onNumberDetected(int countryCode, String nationalNumber) { @@ -118,67 +145,183 @@ public final class RegistrationViewModel extends ViewModel { .build()); } - public String getFcmToken() { - return fcmToken.getValue(); + private @Nullable String getFcmToken() { + return registrationState.get(STATE_FCM_TOKEN); } @MainThread public void setFcmToken(@Nullable String fcmToken) { - this.fcmToken.setValue(fcmToken); + registrationState.set(STATE_FCM_TOKEN, fcmToken); } public void setWelcomeSkippedOnRestore() { - restoreFlowShown.setValue(true); + registrationState.set(STATE_RESTORE_FLOW_SHOWN, true); } public boolean hasRestoreFlowBeenShown() { - //noinspection ConstantConditions Live data was given an initial value - return restoreFlowShown.getValue(); + //noinspection ConstantConditions + return registrationState.get(STATE_RESTORE_FLOW_SHOWN); } - public void markASuccessfulAttempt() { - //noinspection ConstantConditions Live data was given an initial value - successfulCodeRequestAttempts.setValue(successfulCodeRequestAttempts.getValue() + 1); + private void markASuccessfulAttempt() { + //noinspection ConstantConditions + registrationState.set(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, (Integer) registrationState.get(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS) + 1); } public LiveData getSuccessfulCodeRequestAttempts() { - return successfulCodeRequestAttempts; + return registrationState.getLiveData(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0); } - public @NonNull LocalCodeRequestRateLimiter getRequestLimiter() { - //noinspection ConstantConditions Live data was given an initial value - return requestLimiter.getValue(); + private @NonNull LocalCodeRequestRateLimiter getRequestLimiter() { + //noinspection ConstantConditions + return registrationState.get(STATE_REQUEST_RATE_LIMITER); } - public void updateLimiter() { - requestLimiter.setValue(requestLimiter.getValue()); + private void updateLimiter() { + registrationState.set(STATE_REQUEST_RATE_LIMITER, registrationState.get(STATE_REQUEST_RATE_LIMITER)); } public @Nullable TokenData getKeyBackupCurrentToken() { - return kbsTokenData.getValue(); + return registrationState.get(STATE_KBS_TOKEN); } - public void setKeyBackupTokenData(TokenData tokenData) { - kbsTokenData.setValue(tokenData); + public void setKeyBackupTokenData(@Nullable TokenData tokenData) { + registrationState.set(STATE_KBS_TOKEN, tokenData); } public LiveData getLockedTimeRemaining() { - return lockedTimeRemaining; + return registrationState.getLiveData(STATE_TIME_REMAINING, 0L); } public LiveData getCanCallAtTime() { - return canCallAtTime; + return registrationState.getLiveData(STATE_CAN_CALL_AT_TIME, 0L); } public void setLockedTimeRemaining(long lockedTimeRemaining) { - this.lockedTimeRemaining.setValue(lockedTimeRemaining); + registrationState.set(STATE_TIME_REMAINING, lockedTimeRemaining); } public void onStartEnterCode() { - canCallAtTime.setValue(System.currentTimeMillis() + FIRST_CALL_AVAILABLE_AFTER_MS); + registrationState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + FIRST_CALL_AVAILABLE_AFTER_MS); } - public void onCallRequested() { - canCallAtTime.setValue(System.currentTimeMillis() + SUBSEQUENT_CALL_AVAILABLE_AFTER_MS); + private void onCallRequested() { + registrationState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + SUBSEQUENT_CALL_AVAILABLE_AFTER_MS); + } + + public void setIsReregister(boolean isReregister) { + registrationState.set(STATE_IS_REREGISTER, isReregister); + } + + public Single requestVerificationCode(@NonNull Mode mode) { + String captcha = getCaptchaToken(); + clearCaptchaResponse(); + + if (mode == Mode.PHONE_CALL) { + onCallRequested(); + } else if (!getRequestLimiter().canRequest(mode, getNumber().getE164Number(), System.currentTimeMillis())) { + return Single.just(RequestVerificationCodeResponseProcessor.forLocalRateLimit()); + } + + return verifyAccountRepository.requestVerificationCode(getNumber().getE164Number(), + getRegistrationSecret(), + mode, + captcha) + .map(RequestVerificationCodeResponseProcessor::new) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess(processor -> { + if (processor.hasResult()) { + markASuccessfulAttempt(); + setFcmToken(processor.getResult().getFcmToken().orNull()); + getRequestLimiter().onSuccessfulRequest(mode, getNumber().getE164Number(), System.currentTimeMillis()); + } else { + getRequestLimiter().onUnsuccessfulRequest(); + } + updateLimiter(); + }); + } + + public Single verifyCodeAndRegisterAccountWithoutRegistrationLock(@NonNull String code) { + onVerificationCodeEntered(code); + + RegistrationData registrationData = new RegistrationData(getTextCodeEntered(), + getNumber().getE164Number(), + getRegistrationSecret(), + registrationRepository.getRegistrationId(), + registrationRepository.getProfileKey(getNumber().getE164Number()), + getFcmToken()); + + return verifyAccountRepository.verifyAccount(registrationData) + .map(VerifyAccountResponseWithoutKbs::new) + .flatMap(processor -> { + if (processor.hasResult()) { + return registrationRepository.registerAccountWithoutRegistrationLock(registrationData, processor.getResult()) + .map(VerifyAccountResponseWithoutKbs::new); + } else if (processor.registrationLock() && !processor.isKbsLocked()) { + return kbsRepository.getToken(processor.getLockedException().getBasicStorageCredentials()) + .map(r -> r.getResult().isPresent() ? new VerifyAccountResponseWithSuccessfulKbs(processor.getResponse(), r.getResult().get()) + : new VerifyAccountResponseWithFailedKbs(r)); + } + return Single.just(processor); + }) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess(processor -> { + if (processor.registrationLock() && !processor.isKbsLocked()) { + setLockedTimeRemaining(processor.getLockedException().getTimeRemaining()); + setKeyBackupTokenData(processor.getTokenData()); + } else if (processor.isKbsLocked()) { + setLockedTimeRemaining(processor.getLockedException().getTimeRemaining()); + } + }); + + } + + public Single verifyCodeAndRegisterAccountWithRegistrationLock(@NonNull String pin) { + RegistrationData registrationData = new RegistrationData(getTextCodeEntered(), + getNumber().getE164Number(), + getRegistrationSecret(), + registrationRepository.getRegistrationId(), + registrationRepository.getProfileKey(getNumber().getE164Number()), + getFcmToken()); + + TokenData kbsTokenData = Objects.requireNonNull(getKeyBackupCurrentToken()); + + return verifyAccountRepository.verifyAccountWithPin(registrationData, pin, kbsTokenData) + .map(r -> new VerifyCodeWithRegistrationLockResponseProcessor(r, kbsTokenData)) + .flatMap(processor -> { + if (processor.hasResult()) { + return registrationRepository.registerAccountWithRegistrationLock(registrationData, processor.getResult(), pin) + .map(processor::updatedIfRegistrationFailed); + } else if (processor.wrongPin()) { + TokenData newToken = TokenData.withResponse(kbsTokenData, processor.getTokenResponse()); + return Single.just(new VerifyCodeWithRegistrationLockResponseProcessor(processor.getResponse(), newToken)); + } + return Single.just(processor); + }) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess(processor -> { + if (processor.wrongPin()) { + setKeyBackupTokenData(processor.getToken()); + } + }); + } + + public static final class Factory extends AbstractSavedStateViewModelFactory { + private final boolean isReregister; + + public Factory(@NonNull SavedStateRegistryOwner owner, boolean isReregister) { + super(owner, null); + this.isReregister = isReregister; + } + + @Override + protected @NonNull T create(@NonNull String key, @NonNull Class modelClass, @NonNull SavedStateHandle handle) { + //noinspection ConstantConditions + return modelClass.cast(new RegistrationViewModel(handle, + isReregister, + new VerifyAccountRepository(ApplicationDependencies.getApplication()), + new KbsRepository(), + new RegistrationRepository(ApplicationDependencies.getApplication()))); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CircularProgressButtonUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/CircularProgressButtonUtil.kt new file mode 100644 index 0000000000..ca24e6fc43 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CircularProgressButtonUtil.kt @@ -0,0 +1,24 @@ +package org.thoughtcrime.securesms.util + +import com.dd.CircularProgressButton + +object CircularProgressButtonUtil { + + @JvmStatic + fun setSpinning(button: CircularProgressButton?) { + button?.apply { + isClickable = false + isIndeterminateProgressMode = true + progress = 50 + } + } + + @JvmStatic + fun cancelSpinning(button: CircularProgressButton?) { + button?.apply { + progress = 0 + isIndeterminateProgressMode = false + isClickable = true + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt b/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt index dde878e93a..9760748ea4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt @@ -23,6 +23,7 @@ class LifecycleDisposable : DefaultLifecycleObserver { } override fun onDestroy(owner: LifecycleOwner) { + owner.lifecycle.removeObserver(this) disposables.clear() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SignalUncaughtExceptionHandler.java b/app/src/main/java/org/thoughtcrime/securesms/util/SignalUncaughtExceptionHandler.java index a570c0c395..2db4803630 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SignalUncaughtExceptionHandler.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SignalUncaughtExceptionHandler.java @@ -8,6 +8,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import java.util.concurrent.TimeUnit; +import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; + public class SignalUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { private static final String TAG = Log.tag(SignalUncaughtExceptionHandler.class); @@ -20,6 +22,10 @@ public class SignalUncaughtExceptionHandler implements Thread.UncaughtExceptionH @Override public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) { + if (e instanceof OnErrorNotImplementedException) { + e = e.getCause(); + } + Log.e(TAG, "", e, true); SignalStore.blockUntilAllWritesFinished(); Log.blockUntilAllWritesFinished(); diff --git a/app/src/main/res/navigation/registration.xml b/app/src/main/res/navigation/registration.xml index 1a1e618279..3816184b7a 100644 --- a/app/src/main/res/navigation/registration.xml +++ b/app/src/main/res/navigation/registration.xml @@ -116,14 +116,7 @@ android:id="@+id/countryPickerFragment" android:name="org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment" android:label="fragment_country_picker" - tools:layout="@layout/fragment_registration_country_picker"> - - - - + tools:layout="@layout/fragment_registration_country_picker"/> - - - - + tools:layout="@layout/fragment_registration_captcha"/> Signal needs access to your contacts in order to connect with friends, exchange messages, and make secure calls You\'ve made too many attempts to register this number. Please try again later. Unable to connect to service. Please check network connection and try again. + Call requested You are now %d step away from submitting a debug log. You are now %d steps away from submitting a debug log. @@ -3010,6 +3011,7 @@ Call me instead \n (Available in %1$02d:%2$02d) Contact Signal Support Signal Registration - Verification Code for Android + Incorrect code Never Unknown See my phone number diff --git a/app/src/test/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiterTest.java b/app/src/test/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiterTest.java index 9377d7771f..ac4967313c 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiterTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/registration/viewmodel/LocalCodeRequestRateLimiterTest.java @@ -1,10 +1,10 @@ package org.thoughtcrime.securesms.registration.viewmodel; -import org.junit.Test; -import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode; + +import org.junit.Test; public final class LocalCodeRequestRateLimiterTest { @@ -12,63 +12,63 @@ public final class LocalCodeRequestRateLimiterTest { public void initially_can_request() { LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000)); + assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000)); } @Test public void cant_request_within_same_time_period() { LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000)); + assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000)); - limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000); + limiter.onSuccessfulRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000); - assertFalse(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000)); + assertFalse(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000)); } @Test public void can_request_within_same_time_period_if_different_number() { LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000)); + assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000)); - limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000); + limiter.onSuccessfulRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000); - assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+15559874566", 1000 + 59_000)); + assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+15559874566", 1000 + 59_000)); } @Test public void can_request_within_same_time_period_if_different_mode() { LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000)); + assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000)); - limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000); + limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000); - assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000)); + assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000)); } @Test public void can_request_after_time_period() { LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000)); + assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000)); - limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000); + limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000); - assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 60_001)); + assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 60_001)); } @Test public void can_request_within_same_time_period_if_an_unsuccessful_request_is_seen() { LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000); - assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000)); + assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000)); - limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000); + limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000); limiter.onUnsuccessfulRequest(); - assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 59_000)); + assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 59_000)); } } \ No newline at end of file diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 1b5cb41df2..249db61ed4 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -45,6 +45,7 @@ import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.api.storage.StorageManifestKey; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.StreamDetails; +import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher; import org.whispersystems.signalservice.internal.contacts.crypto.Quote; @@ -61,6 +62,7 @@ import org.whispersystems.signalservice.internal.push.ProfileAvatarData; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil; import org.whispersystems.signalservice.internal.push.RemoteConfigResponse; +import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory; @@ -209,10 +211,14 @@ public class SignalServiceAccountManager { * @param androidSmsRetrieverSupported * @param captchaToken If the user has done a CAPTCHA, include this. * @param challenge If present, it can bypass the CAPTCHA. - * @throws IOException */ - public void requestSmsVerificationCode(boolean androidSmsRetrieverSupported, Optional captchaToken, Optional challenge) throws IOException { - this.pushServiceSocket.requestSmsVerificationCode(androidSmsRetrieverSupported, captchaToken, challenge); + public ServiceResponse requestSmsVerificationCode(boolean androidSmsRetrieverSupported, Optional captchaToken, Optional challenge, Optional fcmToken) { + try { + this.pushServiceSocket.requestSmsVerificationCode(androidSmsRetrieverSupported, captchaToken, challenge); + return ServiceResponse.forResult(new RequestVerificationCodeResponse(fcmToken), 200, null); + } catch (IOException e) { + return ServiceResponse.forUnknownError(e); + } } /** @@ -222,10 +228,14 @@ public class SignalServiceAccountManager { * @param locale * @param captchaToken If the user has done a CAPTCHA, include this. * @param challenge If present, it can bypass the CAPTCHA. - * @throws IOException */ - public void requestVoiceVerificationCode(Locale locale, Optional captchaToken, Optional challenge) throws IOException { - this.pushServiceSocket.requestVoiceVerificationCode(locale, captchaToken, challenge); + public ServiceResponse requestVoiceVerificationCode(Locale locale, Optional captchaToken, Optional challenge, Optional fcmToken) { + try { + this.pushServiceSocket.requestVoiceVerificationCode(locale, captchaToken, challenge); + return ServiceResponse.forResult(new RequestVerificationCodeResponse(fcmToken), 200, null); + } catch (IOException e) { + return ServiceResponse.forUnknownError(e); + } } /** @@ -234,32 +244,76 @@ public class SignalServiceAccountManager { * @param verificationCode The verification code received via SMS or Voice * (see {@link #requestSmsVerificationCode} and * {@link #requestVoiceVerificationCode}). - * @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key, - * concatenated. * @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install. * This value should remain consistent across registrations for the * same install, but probabilistically differ across registrations * for separate installs. - * @param pin Deprecated, only supply the pin if you did not find a registrationLock on KBS. + * @return The UUID of the user that was registered. + * @throws IOException for various HTTP and networking errors + */ + public ServiceResponse verifyAccount(String verificationCode, + int signalProtocolRegistrationId, + boolean fetchesMessages, + byte[] unidentifiedAccessKey, + boolean unrestrictedUnidentifiedAccess, + AccountAttributes.Capabilities capabilities, + boolean discoverableByPhoneNumber) + { + try { + VerifyAccountResponse response = this.pushServiceSocket.verifyAccountCode(verificationCode, + null, + signalProtocolRegistrationId, + fetchesMessages, + null, + null, + unidentifiedAccessKey, + unrestrictedUnidentifiedAccess, + capabilities, + discoverableByPhoneNumber); + return ServiceResponse.forResult(response, 200, null); + } catch (IOException e) { + return ServiceResponse.forUnknownError(e); + } + } + + /** + * Verify a Signal Service account with a received SMS or voice verification code with + * registration lock. + * + * @param verificationCode The verification code received via SMS or Voice + * (see {@link #requestSmsVerificationCode} and + * {@link #requestVoiceVerificationCode}). + * @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install. + * This value should remain consistent across registrations for the + * same install, but probabilistically differ across registrations + * for separate installs. * @param registrationLock Only supply if found on KBS. * @return The UUID of the user that was registered. - * @throws IOException */ - public VerifyAccountResponse verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages, - String pin, String registrationLock, - byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess, - AccountAttributes.Capabilities capabilities, - boolean discoverableByPhoneNumber) - throws IOException + public ServiceResponse verifyAccountWithRegistrationLockPin(String verificationCode, + int signalProtocolRegistrationId, + boolean fetchesMessages, + String registrationLock, + byte[] unidentifiedAccessKey, + boolean unrestrictedUnidentifiedAccess, + AccountAttributes.Capabilities capabilities, + boolean discoverableByPhoneNumber) { - return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey, - signalProtocolRegistrationId, - fetchesMessages, - pin, registrationLock, - unidentifiedAccessKey, - unrestrictedUnidentifiedAccess, - capabilities, - discoverableByPhoneNumber); + try { + VerifyAccountResponse response = this.pushServiceSocket.verifyAccountCode(verificationCode, + null, + signalProtocolRegistrationId, + fetchesMessages, + null, + registrationLock, + unidentifiedAccessKey, + unrestrictedUnidentifiedAccess, + capabilities, + discoverableByPhoneNumber); + return ServiceResponse.forResult(response, 200, null); + } catch (IOException e) { + return ServiceResponse.forUnknownError(e); + } } /** diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/LocalRateLimitException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/LocalRateLimitException.java new file mode 100644 index 0000000000..3426d01e4c --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/LocalRateLimitException.java @@ -0,0 +1,8 @@ +package org.whispersystems.signalservice.api.push.exceptions; + +/** + * Thrown when self limiting networking. + */ +public final class LocalRateLimitException extends Exception { + public LocalRateLimitException() { } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponse.java index 0f9fbb3b66..7338ab3466 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponse.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponse.java @@ -3,6 +3,7 @@ package org.whispersystems.signalservice.internal; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Preconditions; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.internal.websocket.WebsocketResponse; import java.util.concurrent.ExecutionException; @@ -92,8 +93,17 @@ public final class ServiceResponse { return forUnknownError(throwable.getCause()); } else if (throwable instanceof NonSuccessfulResponseCodeException) { return forApplicationError(throwable, ((NonSuccessfulResponseCodeException) throwable).getCode(), null); + } else if (throwable instanceof PushNetworkException && throwable.getCause() != null) { + return forUnknownError(throwable.getCause()); } else { return forExecutionError(throwable); } } + + public static ServiceResponse coerceError(ServiceResponse response) { + if (response.applicationError.isPresent()) { + return ServiceResponse.forApplicationError(response.applicationError.get(), response.status, response.body.orNull()); + } + return ServiceResponse.forExecutionError(response.executionError.orNull()); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponseProcessor.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponseProcessor.java index 67cb6929ed..b316633a25 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponseProcessor.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/ServiceResponseProcessor.java @@ -65,6 +65,10 @@ public abstract class ServiceResponseProcessor { return response.getStatus() == 401 || response.getStatus() == 403; } + protected boolean captchaRequired() { + return response.getStatus() == 402; + } + protected boolean notFound() { return response.getStatus() == 404; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RequestVerificationCodeResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RequestVerificationCodeResponse.java new file mode 100644 index 0000000000..c69778766b --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/RequestVerificationCodeResponse.java @@ -0,0 +1,16 @@ +package org.whispersystems.signalservice.internal.push; + + +import org.whispersystems.libsignal.util.guava.Optional; + +public final class RequestVerificationCodeResponse { + private final Optional fcmToken; + + public RequestVerificationCodeResponse(Optional fcmToken) { + this.fcmToken = fcmToken; + } + + public Optional getFcmToken() { + return fcmToken; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/DefaultErrorMapper.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/DefaultErrorMapper.java index 683fd163f9..b357ba7309 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/DefaultErrorMapper.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/websocket/DefaultErrorMapper.java @@ -2,6 +2,7 @@ package org.whispersystems.signalservice.internal.websocket; import org.whispersystems.libsignal.util.guava.Function; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; +import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException; import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException; import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException; @@ -72,6 +73,8 @@ public final class DefaultErrorMapper implements ErrorMapper { case 401: case 403: return new AuthorizationFailedException(status, "Authorization failed!"); + case 402: + return new CaptchaRequiredException(); case 404: return new NotFoundException("Not found"); case 409: