From 4f458a022f175988aa13595d215604208673d91d Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 22 Feb 2023 11:03:10 -0500 Subject: [PATCH] Add skip SMS flow. --- .../securesms/ApplicationContext.java | 7 +- .../securesms/jobs/JobManagerFactories.java | 1 + .../securesms/jobs/RefreshAttributesJob.java | 49 ++--- .../jobs/RefreshKbsCredentialsJob.kt | 65 ++++++ .../securesms/keyvalue/KbsValues.java | 51 +++-- .../securesms/pin/KbsRepository.java | 22 ++- .../thoughtcrime/securesms/pin/PinState.java | 15 +- .../registration/RegistrationRepository.java | 28 ++- .../registration/VerifyAccountRepository.kt | 2 +- .../BaseRegistrationLockFragment.java | 2 +- .../fragments/EnterPhoneNumberFragment.java | 22 ++- .../fragments/ReRegisterWithPinFragment.kt | 135 +++++++++++++ .../viewmodel/BaseRegistrationViewModel.java | 12 +- .../viewmodel/RegistrationViewModel.java | 186 +++++++++++++++++- .../res/layout/fragment_registration_lock.xml | 3 +- app/src/main/res/navigation/registration.xml | 37 +++- .../api/SignalServiceAccountManager.java | 57 +++--- .../internal/push/BackupAuthCheckRequest.kt | 46 +++++ .../internal/push/PushServiceSocket.java | 48 ++--- 19 files changed, 657 insertions(+), 131 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshKbsCredentialsJob.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckRequest.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 75ed379f29..d040b0dd0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -24,7 +24,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; -import androidx.appcompat.app.AppCompatDelegate; import androidx.multidex.MultiDexApplication; import com.google.android.gms.security.ProviderInstaller; @@ -63,6 +62,7 @@ import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob; import org.thoughtcrime.securesms.jobs.PreKeysSyncJob; import org.thoughtcrime.securesms.jobs.ProfileUploadJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; +import org.thoughtcrime.securesms.jobs.RefreshKbsCredentialsJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob; import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob; @@ -75,7 +75,6 @@ import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver; import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.mms.SignalGlideComponents; import org.thoughtcrime.securesms.mms.SignalGlideModule; -import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.ratelimit.RateLimitUtil; import org.thoughtcrime.securesms.recipients.Recipient; @@ -104,13 +103,10 @@ import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWra import java.net.SocketException; import java.net.SocketTimeoutException; import java.security.Security; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; -import io.reactivex.rxjava3.core.CompletableObserver; -import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException; import io.reactivex.rxjava3.exceptions.UndeliverableException; import io.reactivex.rxjava3.plugins.RxJavaPlugins; @@ -202,6 +198,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr .addPostRender(this::initializeExpiringMessageManager) .addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this))) .addPostRender(this::initializeTrimThreadsByDateManager) + .addPostRender(RefreshKbsCredentialsJob::enqueueIfNecessary) .addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this)) .addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary) .addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge())) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 79d4561b29..0ac83927b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -162,6 +162,7 @@ public final class JobManagerFactories { put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory()); put(ReactionSendJob.KEY, new ReactionSendJob.Factory()); put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory()); + put(RefreshKbsCredentialsJob.KEY, new RefreshKbsCredentialsJob.Factory()); put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory()); put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory()); put(RemoteDeleteSendJob.KEY, new RemoteDeleteSendJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java index eb46046b0e..99cb09584d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshAttributesJob.java @@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; +import org.whispersystems.util.Base64; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -83,15 +84,15 @@ public class RefreshAttributesJob extends BaseJob { return; } - int registrationId = SignalStore.account().getRegistrationId(); - boolean fetchesMessages = !SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced(); - byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); - boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context); - String registrationLockV1 = null; - String registrationLockV2 = null; - KbsValues kbsValues = SignalStore.kbsValues(); - int pniRegistrationId = new RegistrationRepository(ApplicationDependencies.getApplication()).getPniRegistrationId(); - String registrationRecoveryPassword = kbsValues.getRegistrationRecoveryPassword(); + int registrationId = SignalStore.account().getRegistrationId(); + boolean fetchesMessages = !SignalStore.account().isFcmEnabled() || SignalStore.internalValues().isWebsocketModeForced(); + byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getSelfProfileKey()); + boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context); + String registrationLockV1 = null; + String registrationLockV2 = null; + KbsValues kbsValues = SignalStore.kbsValues(); + int pniRegistrationId = new RegistrationRepository(ApplicationDependencies.getApplication()).getPniRegistrationId(); + String recoveryPassword = kbsValues.getRecoveryPassword(); if (kbsValues.isV2RegistrationLockEnabled()) { registrationLockV2 = kbsValues.getRegistrationLockToken(); @@ -107,23 +108,27 @@ public class RefreshAttributesJob extends BaseJob { AccountAttributes.Capabilities capabilities = AppCapabilities.getCapabilities(kbsValues.hasPin() && !kbsValues.hasOptedOut()); Log.i(TAG, "Calling setAccountAttributes() reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2) + ", pin? " + kbsValues.hasPin() + + "\n Recovery password? " + !TextUtils.isEmpty(recoveryPassword) + "\n Phone number discoverable : " + phoneNumberDiscoverable + "\n Device Name : " + (encryptedDeviceName != null) + "\n Capabilities: " + capabilities); - SignalServiceAccountManager signalAccountManager = ApplicationDependencies.getSignalServiceAccountManager(); - signalAccountManager.setAccountAttributes(null, - registrationId, - fetchesMessages, - registrationLockV1, - registrationLockV2, - unidentifiedAccessKey, - universalUnidentifiedAccess, - capabilities, - phoneNumberDiscoverable, - encryptedDeviceName, - pniRegistrationId, - registrationRecoveryPassword); + AccountAttributes accountAttributes = new AccountAttributes( + null, + registrationId, + fetchesMessages, + registrationLockV1, + registrationLockV2, + unidentifiedAccessKey, + universalUnidentifiedAccess, + capabilities, + phoneNumberDiscoverable, + (encryptedDeviceName == null) ? null : Base64.encodeBytes(encryptedDeviceName), + pniRegistrationId, + recoveryPassword + ); + + ApplicationDependencies.getSignalServiceAccountManager().setAccountAttributes(accountAttributes); hasRefreshedThisAppCycle = true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshKbsCredentialsJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshKbsCredentialsJob.kt new file mode 100644 index 0000000000..a05f1e47b0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RefreshKbsCredentialsJob.kt @@ -0,0 +1,65 @@ +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobmanager.Data +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.pin.KbsRepository +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import java.io.IOException +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days + +/** + * Refresh KBS authentication credentials for talking to KBS during re-registration. + */ +class RefreshKbsCredentialsJob private constructor(parameters: Parameters) : BaseJob(parameters) { + + companion object { + const val KEY = "RefreshKbsCredentialsJob" + + private val TAG = Log.tag(RefreshKbsCredentialsJob::class.java) + private val FREQUENCY: Duration = 15.days + + @JvmStatic + fun enqueueIfNecessary() { + val lastTimestamp = SignalStore.kbsValues().lastRefreshAuthTimestamp + if (lastTimestamp + FREQUENCY.inWholeMilliseconds < System.currentTimeMillis() || lastTimestamp > System.currentTimeMillis()) { + ApplicationDependencies.getJobManager().add(RefreshKbsCredentialsJob()) + } else { + Log.d(TAG, "Do not need to refresh credentials. Last refresh: $lastTimestamp") + } + } + } + + private constructor() : this( + parameters = Parameters.Builder() + .setQueue("RefreshKbsCredentials") + .addConstraint(NetworkConstraint.KEY) + .setMaxInstancesForQueue(2) + .setLifespan(1.days.inWholeMilliseconds) + .build() + ) + + override fun serialize(): Data = Data.Builder().build() + + override fun getFactoryKey(): String = KEY + + override fun onRun() { + KbsRepository().refreshAuthorization() + } + + override fun onShouldRetry(e: Exception): Boolean { + return e is IOException && e !is NonSuccessfulResponseCodeException + } + + override fun onFailure() = Unit + + class Factory : Job.Factory { + override fun create(parameters: Parameters, data: Data): RefreshKbsCredentialsJob { + return RefreshKbsCredentialsJob(parameters) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java index 58a2f0c079..712e4e0dc3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java @@ -12,6 +12,7 @@ import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse import java.io.IOException; import java.security.SecureRandom; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -19,15 +20,16 @@ import java.util.stream.Stream; public final class KbsValues extends SignalStoreValues { - public static final String V2_LOCK_ENABLED = "kbs.v2_lock_enabled"; - private static final String MASTER_KEY = "kbs.registration_lock_master_key"; - private static final String TOKEN_RESPONSE = "kbs.token_response"; - private static final String PIN = "kbs.pin"; - private static final String LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash"; - private static final String LAST_CREATE_FAILED_TIMESTAMP = "kbs.last_create_failed_timestamp"; - public static final String OPTED_OUT = "kbs.opted_out"; - private static final String PIN_FORGOTTEN_OR_SKIPPED = "kbs.pin.forgotten.or.skipped"; - private static final String KBS_AUTH_TOKENS = "kbs.kbs_auth_tokens"; + public static final String V2_LOCK_ENABLED = "kbs.v2_lock_enabled"; + private static final String MASTER_KEY = "kbs.registration_lock_master_key"; + private static final String TOKEN_RESPONSE = "kbs.token_response"; + private static final String PIN = "kbs.pin"; + private static final String LOCK_LOCAL_PIN_HASH = "kbs.registration_lock_local_pin_hash"; + private static final String LAST_CREATE_FAILED_TIMESTAMP = "kbs.last_create_failed_timestamp"; + public static final String OPTED_OUT = "kbs.opted_out"; + private static final String PIN_FORGOTTEN_OR_SKIPPED = "kbs.pin.forgotten.or.skipped"; + private static final String KBS_AUTH_TOKENS = "kbs.kbs_auth_tokens"; + private static final String KBS_LAST_AUTH_REFRESH_TIMESTAMP = "kbs.kbs_auth_tokens.last_refresh_timestamp"; KbsValues(KeyValueStore store) { super(store); @@ -55,6 +57,8 @@ public final class KbsValues extends SignalStoreValues { .remove(PIN) .remove(LAST_CREATE_FAILED_TIMESTAMP) .remove(OPTED_OUT) + .remove(KBS_AUTH_TOKENS) + .remove(KBS_LAST_AUTH_REFRESH_TIMESTAMP) .commit(); } @@ -149,12 +153,12 @@ public final class KbsValues extends SignalStoreValues { } } - public synchronized @Nullable String getRegistrationRecoveryPassword() { - MasterKey masterKey = getPinBackedMasterKey(); - if (masterKey == null) { - return null; - } else { + public synchronized @Nullable String getRecoveryPassword() { + MasterKey masterKey = getMasterKey(); + if (masterKey != null && hasPin()) { return masterKey.deriveRegistrationRecoveryPassword(); + } else { + return null; } } @@ -180,6 +184,7 @@ public final class KbsValues extends SignalStoreValues { public synchronized void putAuthTokenList(List tokens) { putList(KBS_AUTH_TOKENS, tokens, StringStringSerializer.INSTANCE); + setLastRefreshAuthTimestamp(System.currentTimeMillis()); } public synchronized List getKbsAuthTokenList() { @@ -202,6 +207,16 @@ public final class KbsValues extends SignalStoreValues { } } + public boolean removeAuthTokens(@NonNull List invalid) { + List tokens = new ArrayList<>(getKbsAuthTokenList()); + if (tokens.removeAll(invalid)) { + putAuthTokenList(tokens); + return true; + } + + return false; + } + /** Should only be called by {@link org.thoughtcrime.securesms.pin.PinState}. */ public synchronized void optOut() { getStore().beginWrite() @@ -229,4 +244,12 @@ public final class KbsValues extends SignalStoreValues { throw new AssertionError(e); } } + + private void setLastRefreshAuthTimestamp(long timestamp) { + putLong(KBS_LAST_AUTH_REFRESH_TIMESTAMP, timestamp); + } + + public long getLastRefreshAuthTimestamp() { + return getLong(KBS_LAST_AUTH_REFRESH_TIMESTAMP, 0L); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java b/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java index bbcade0270..378d821733 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/KbsRepository.java @@ -63,6 +63,26 @@ public class KbsRepository { }).subscribeOn(Schedulers.io()); } + /** + * Fetch and store a new KBS authorization. + */ + public void refreshAuthorization() throws IOException { + for (KbsEnclave enclave : KbsEnclaves.all()) { + KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave); + + try { + String authorization = kbs.getAuthorization(); + backupAuthToken(authorization); + } catch (NonSuccessfulResponseCodeException e) { + if (e.getCode() == 404) { + Log.i(TAG, "Enclave decommissioned, skipping", e); + } else { + throw e; + } + } + } + } + private @NonNull TokenData getTokenSync(@Nullable String authorization) throws IOException { TokenData firstKnownTokenData = null; @@ -101,7 +121,7 @@ public class KbsRepository { private static void backupAuthToken(String token) { final boolean tokenIsNew = SignalStore.kbsValues().appendAuthTokenToList(token); - if (tokenIsNew) { + if (tokenIsNew && SignalStore.kbsValues().hasPin()) { new BackupManager(ApplicationDependencies.getApplication()).dataChanged(); } } 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 c056ccff3e..482cf5a6c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java @@ -29,7 +29,6 @@ import org.whispersystems.signalservice.internal.contacts.crypto.Unauthenticated import java.io.IOException; import java.util.Arrays; -import java.util.Collections; import java.util.Locale; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -44,7 +43,8 @@ public final class PinState { public static synchronized void onRegistration(@NonNull Context context, @Nullable KbsPinData kbsData, @Nullable String pin, - boolean hasPinToRestore) + boolean hasPinToRestore, + boolean setRegistrationLockEnabled) { Log.i(TAG, "onRegistration()"); @@ -57,9 +57,13 @@ public final class PinState { TextSecurePreferences.setRegistrationLockLastReminderTime(context, System.currentTimeMillis()); TextSecurePreferences.setRegistrationLockNextReminderInterval(context, RegistrationLockReminders.INITIAL_INTERVAL); } else if (kbsData != null && pin != null) { - Log.i(TAG, "Registration Lock V2"); - TextSecurePreferences.setV1RegistrationLockEnabled(context, false); - SignalStore.kbsValues().setV2RegistrationLockEnabled(true); + if (setRegistrationLockEnabled) { + Log.i(TAG, "Registration Lock V2"); + TextSecurePreferences.setV1RegistrationLockEnabled(context, false); + SignalStore.kbsValues().setV2RegistrationLockEnabled(true); + } else { + Log.i(TAG, "ReRegistration Skip SMS"); + } SignalStore.kbsValues().setKbsMasterKey(kbsData, pin); SignalStore.pinValues().resetPinReminders(); resetPinRetryCount(context, pin); @@ -130,6 +134,7 @@ public final class PinState { bestEffortRefreshAttributes(); } else { Log.i(TAG, "Not the first time setting a PIN. Enclave: " + kbsEnclave.getEnclaveName()); + ApplicationDependencies.getJobManager().add(new RefreshAttributesJob()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java index f1391937e6..b4847948e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationRepository.java @@ -1,6 +1,7 @@ package org.thoughtcrime.securesms.registration; import android.app.Application; +import android.app.backup.BackupManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -41,6 +42,7 @@ import org.whispersystems.signalservice.api.push.PNI; import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.ServiceResponse; +import org.whispersystems.signalservice.internal.push.BackupAuthCheckProcessor; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import java.io.IOException; @@ -95,12 +97,13 @@ public final class RegistrationRepository { } public Single> registerAccount(@NonNull RegistrationData registrationData, - @NonNull VerifyResponse response) + @NonNull VerifyResponse response, + boolean setRegistrationLockEnabled) { return Single.>fromCallable(() -> { try { String pin = response.getPin(); - registerAccountInternal(registrationData, response.getVerifyAccountResponse(), pin, response.getKbsData()); + registerAccountInternal(registrationData, response.getVerifyAccountResponse(), pin, response.getKbsData(), setRegistrationLockEnabled); if (pin != null && !pin.isEmpty()) { PinState.onPinChangedOrCreated(context, pin, SignalStore.pinValues().getKeyboardType()); @@ -124,7 +127,8 @@ public final class RegistrationRepository { private void registerAccountInternal(@NonNull RegistrationData registrationData, @NonNull VerifyAccountResponse response, @Nullable String pin, - @Nullable KbsPinData kbsData) + @Nullable KbsPinData kbsData, + boolean setRegistrationLockEnabled) throws IOException { ACI aci = ACI.parseOrThrow(response.getUuid()); @@ -172,7 +176,10 @@ public final class RegistrationRepository { TextSecurePreferences.setPromptedPushRegistration(context, true); NotificationManagerCompat.from(context).cancel(NotificationIds.UNREGISTERED_NOTIFICATION_ID); - PinState.onRegistration(context, kbsData, pin, hasPin); + PinState.onRegistration(context, kbsData, pin, hasPin, setRegistrationLockEnabled); + + ApplicationDependencies.closeConnections(); + ApplicationDependencies.getIncomingMessageObserver(); } private void generateAndRegisterPreKeys(@NonNull ServiceIdType serviceIdType, @@ -210,8 +217,15 @@ public final class RegistrationRepository { return null; } - @Nullable - public String getRecoveryPassword() { - return SignalStore.kbsValues().getRegistrationRecoveryPassword(); + public Single getKbsAuthCredential(@NonNull RegistrationData registrationData) { + SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, registrationData.getE164(), SignalServiceAddress.DEFAULT_DEVICE_ID, registrationData.getPassword()); + + return accountManager.checkBackupAuthCredentials(registrationData.getE164(), SignalStore.kbsValues().getKbsAuthTokenList()) + .map(BackupAuthCheckProcessor::new) + .doOnSuccess(processor -> { + if (SignalStore.kbsValues().removeAuthTokens(processor.getInvalid())) { + new BackupManager(context).dataChanged(); + } + }); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt index 68424ba0cc..ebf99f134d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/VerifyAccountRepository.kt @@ -147,7 +147,7 @@ class VerifyAccountRepository(private val context: Application) { }.subscribeOn(Schedulers.io()) } - fun registerAccount(sessionId: String, registrationData: RegistrationData, pin: String? = null, kbsPinDataProducer: KbsPinDataProducer? = null): Single> { + fun registerAccount(sessionId: String?, registrationData: RegistrationData, pin: String? = null, kbsPinDataProducer: KbsPinDataProducer? = null): Single> { val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context) val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey) diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java index 3c4bf3f391..bcf2ef9345 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/BaseRegistrationLockFragment.java @@ -44,7 +44,7 @@ public abstract class BaseRegistrationLockFragment extends LoggingFragment { /** * 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; + public static final int MINIMUM_PIN_LENGTH = 4; private EditText pinEntry; private View forgotPin; 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 3bae2b196b..817c6f2b09 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 @@ -180,7 +180,7 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R PlayServicesUtil.PlayServicesStatus fcmStatus = PlayServicesUtil.getPlayServicesStatus(context); if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS) { - confirmNumberPrompt(context, e164number, () -> handleRequestVerification(context, true)); + confirmNumberPrompt(context, e164number, () -> onE164EnteredSuccessfully(context, true)); } else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) { confirmNumberPrompt(context, e164number, () -> handlePromptForNoPlayServices(context)); } else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE) { @@ -192,10 +192,26 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R } } - private void handleRequestVerification(@NonNull Context context, boolean fcmSupported) { + private void onE164EnteredSuccessfully(@NonNull Context context, boolean fcmSupported) { register.setSpinning(); disableAllEntries(); + Disposable disposable = viewModel.canEnterSkipSmsFlow() + .observeOn(AndroidSchedulers.mainThread()) + .onErrorReturnItem(false) + .subscribe(canEnter -> { + if (canEnter) { + Log.i(TAG, "Enter skip flow"); + SafeNavigation.safeNavigate(NavHostFragment.findNavController(this), EnterPhoneNumberFragmentDirections.actionReRegisterWithPinFragment()); + } else { + Log.i(TAG, "Unable to collect necessary data to enter skip flow, returning to normal"); + handleRequestVerification(context, fcmSupported); + } + }); + disposables.add(disposable); + } + + private void handleRequestVerification(@NonNull Context context, boolean fcmSupported) { if (fcmSupported) { SmsRetrieverClient client = SmsRetriever.getClient(context); Task task = client.startSmsRetriever(); @@ -378,7 +394,7 @@ public final class EnterPhoneNumberFragment extends LoggingFragment implements R 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)) + .setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> onE164EnteredSuccessfully(context, false)) .setNegativeButton(android.R.string.cancel, null) .show(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt new file mode 100644 index 0000000000..aee246e3a5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/ReRegisterWithPinFragment.kt @@ -0,0 +1,135 @@ +package org.thoughtcrime.securesms.registration.fragments + +import android.os.Bundle +import android.text.InputType +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.LoggingFragment +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding +import org.thoughtcrime.securesms.lock.v2.PinKeyboardType +import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel +import org.thoughtcrime.securesms.util.LifecycleDisposable +import org.thoughtcrime.securesms.util.ServiceUtil +import org.thoughtcrime.securesms.util.ViewUtil +import org.thoughtcrime.securesms.util.navigation.safeNavigate + +/** + * Using a recovery password or restored KBS token attempt to register in the skip flow. + */ +class ReRegisterWithPinFragment : LoggingFragment(R.layout.fragment_registration_lock) { + + companion object { + private val TAG = Log.tag(ReRegisterWithPinFragment::class.java) + } + + private var _binding: FragmentRegistrationLockBinding? = null + private val binding: FragmentRegistrationLockBinding + get() = _binding!! + + private val viewModel: RegistrationViewModel by activityViewModels() + private val disposables = LifecycleDisposable() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + _binding = FragmentRegistrationLockBinding.bind(view) + + disposables.bindTo(viewLifecycleOwner.lifecycle) + + RegistrationViewDelegate.setDebugLogSubmitMultiTapView(binding.kbsLockPinTitle) + + binding.kbsLockForgotPin.visibility = View.GONE + + binding.kbsLockPinInput.imeOptions = EditorInfo.IME_ACTION_DONE + binding.kbsLockPinInput.setOnEditorActionListener { v, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE) { + ViewUtil.hideKeyboard(requireContext(), v!!) + handlePinEntry() + return@setOnEditorActionListener true + } + false + } + + enableAndFocusPinEntry() + + binding.kbsLockPinConfirm.setOnClickListener { + ViewUtil.hideKeyboard(requireContext(), binding.kbsLockPinInput) + handlePinEntry() + } + + binding.kbsLockKeyboardToggle.setOnClickListener { v: View? -> + val keyboardType: PinKeyboardType = getPinEntryKeyboardType() + updateKeyboard(keyboardType.other) + binding.kbsLockKeyboardToggle.setText(resolveKeyboardToggleText(keyboardType)) + } + + val keyboardType: PinKeyboardType = getPinEntryKeyboardType().other + binding.kbsLockKeyboardToggle.setText(resolveKeyboardToggleText(keyboardType)) + } + + private fun handlePinEntry() { + binding.kbsLockPinInput.isEnabled = false + + val pin: String? = binding.kbsLockPinInput.text?.toString() + + val trimmedLength = pin?.replace(" ", "")?.length ?: 0 + if (trimmedLength == 0) { + Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show() + enableAndFocusPinEntry() + return + } + + if (trimmedLength < BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH) { + Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, BaseRegistrationLockFragment.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show() + enableAndFocusPinEntry() + return + } + + binding.kbsLockPinConfirm.setSpinning() + + disposables += viewModel.verifyReRegisterWithPin(pin!!) + .subscribe { p -> + if (p.hasResult()) { + Log.i(TAG, "Successfully re-registered via skip flow") + findNavController().safeNavigate(R.id.action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment) + } else { + Log.w(TAG, "Unable to continue skip flow, resuming normal flow", p.error) + // todo handle the various error conditions + Toast.makeText(requireContext(), "retry or nav TODO ERROR See log", Toast.LENGTH_SHORT).show() + binding.kbsLockPinInput.isEnabled = true + } + } + } + + private fun enableAndFocusPinEntry() { + binding.kbsLockPinInput.isEnabled = true + binding.kbsLockPinInput.isFocusable = true + if (binding.kbsLockPinInput.requestFocus()) { + ServiceUtil.getInputMethodManager(binding.kbsLockPinInput.context).showSoftInput(binding.kbsLockPinInput, 0) + } + } + + private fun getPinEntryKeyboardType(): PinKeyboardType { + val isNumeric = binding.kbsLockPinInput.inputType and InputType.TYPE_MASK_CLASS == InputType.TYPE_CLASS_NUMBER + return if (isNumeric) PinKeyboardType.NUMERIC else PinKeyboardType.ALPHA_NUMERIC + } + + private fun updateKeyboard(keyboard: PinKeyboardType) { + val isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC + binding.kbsLockPinInput.inputType = if (isAlphaNumeric) InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD else InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD + binding.kbsLockPinInput.text.clear() + } + + @StringRes + private fun resolveKeyboardToggleText(keyboard: PinKeyboardType): Int { + return if (keyboard == PinKeyboardType.ALPHA_NUMERIC) { + R.string.RegistrationLockFragment__enter_alphanumeric_pin + } else { + R.string.RegistrationLockFragment__enter_numeric_pin + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/BaseRegistrationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/BaseRegistrationViewModel.java index fb4d11b401..8e8a5bcf84 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/BaseRegistrationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/BaseRegistrationViewModel.java @@ -42,6 +42,7 @@ public abstract class BaseRegistrationViewModel extends ViewModel { 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_CAN_SMS_AT_TIME = "CAN_SMS_AT_TIME"; + private static final String STATE_RECOVERY_PASSWORD = "RECOVERY_PASSWORD"; protected final SavedStateHandle savedState; protected final VerifyAccountRepository verifyAccountRepository; @@ -62,9 +63,10 @@ public abstract class BaseRegistrationViewModel extends ViewModel { setInitialDefaultValue(STATE_VERIFICATION_CODE, ""); setInitialDefaultValue(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0); setInitialDefaultValue(STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000)); + setInitialDefaultValue(STATE_RECOVERY_PASSWORD, SignalStore.kbsValues().getRecoveryPassword()); } - protected void setInitialDefaultValue(@NonNull String key, @NonNull T initialValue) { + protected void setInitialDefaultValue(@NonNull String key, @Nullable T initialValue) { if (!savedState.contains(key) || savedState.get(key) == null) { savedState.set(key, initialValue); } @@ -164,6 +166,14 @@ public abstract class BaseRegistrationViewModel extends ViewModel { savedState.set(STATE_KBS_TOKEN, tokenData); } + public void setRecoveryPassword(@Nullable String recoveryPassword) { + savedState.set(STATE_RECOVERY_PASSWORD, recoveryPassword); + } + + public @Nullable String getRecoveryPassword() { + return savedState.get(STATE_RECOVERY_PASSWORD); + } + public LiveData getLockedTimeRemaining() { return savedState.getLiveData(STATE_TIME_REMAINING, 0L); } 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 7945c414ef..83b80c9370 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,14 +3,21 @@ package org.thoughtcrime.securesms.registration.viewmodel; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; import androidx.lifecycle.AbstractSavedStateViewModelFactory; import androidx.lifecycle.SavedStateHandle; import androidx.lifecycle.ViewModel; import androidx.savedstate.SavedStateRegistryOwner; +import org.signal.core.util.Stopwatch; +import org.signal.core.util.logging.Log; 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.PinHashing; import org.thoughtcrime.securesms.pin.KbsRepository; +import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException; import org.thoughtcrime.securesms.pin.TokenData; import org.thoughtcrime.securesms.registration.RegistrationData; import org.thoughtcrime.securesms.registration.RegistrationRepository; @@ -19,13 +26,18 @@ import org.thoughtcrime.securesms.registration.VerifyAccountRepository; import org.thoughtcrime.securesms.registration.VerifyResponse; import org.thoughtcrime.securesms.registration.VerifyResponseProcessor; import org.thoughtcrime.securesms.registration.VerifyResponseWithRegistrationLockProcessor; +import org.thoughtcrime.securesms.registration.VerifyResponseWithSuccessfulKbs; import org.thoughtcrime.securesms.registration.VerifyResponseWithoutKbs; +import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.KbsPinData; +import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.push.RegistrationSessionMetadataResponse; +import java.io.IOException; import java.util.Objects; +import java.util.concurrent.TimeUnit; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; @@ -33,6 +45,8 @@ import io.reactivex.rxjava3.schedulers.Schedulers; public final class RegistrationViewModel extends BaseRegistrationViewModel { + private static final String TAG = Log.tag(RegistrationViewModel.class); + 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_IS_REREGISTER = "IS_REREGISTER"; @@ -113,7 +127,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel { setCanCallAtTime(processor.getNextCodeViaCallAttempt()); }) .observeOn(Schedulers.io()) - .flatMap( processor -> { + .flatMap(processor -> { if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) { return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), null, null); } else { @@ -155,7 +169,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel { setCanCallAtTime(processor.getNextCodeViaCallAttempt()); } }) - .flatMap( processor -> { + .flatMap(processor -> { if (processor.isAlreadyVerified() || (processor.hasResult() && processor.isVerified())) { return verifyAccountRepository.registerAccount(sessionId, getRegistrationData(), pin, () -> Objects.requireNonNull(KbsRepository.restoreMasterKey(pin, kbsTokenData.getEnclave(), kbsTokenData.getBasicAuth(), kbsTokenData.getTokenResponse()))); } else { @@ -166,13 +180,13 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel { @Override protected Single onVerifySuccess(@NonNull VerifyResponseProcessor processor) { - return registrationRepository.registerAccount(getRegistrationData(), processor.getResult()) + return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), false) .map(VerifyResponseWithoutKbs::new); } @Override protected Single onVerifySuccessWithRegistrationLock(@NonNull VerifyResponseWithRegistrationLockProcessor processor, String pin) { - return registrationRepository.registerAccount(getRegistrationData(), processor.getResult()) + return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), true) .map(processor::updatedIfRegistrationFailed); } @@ -184,7 +198,169 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel { registrationRepository.getProfileKey(getNumber().getE164Number()), getFcmToken(), registrationRepository.getPniRegistrationId(), - registrationRepository.getRecoveryPassword()); + getRecoveryPassword()); + } + + public @NonNull Single verifyReRegisterWithPin(@NonNull String pin) { + return Single.fromCallable(() -> verifyReRegisterWithPinInternal(pin)) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .flatMap(data -> { + if (data.canProceed) { + return verifyReRegisterWithRecoveryPassword(pin, data.pinData); + } else { + throw new IllegalStateException("Unable to get token or master key"); + } + }) + .onErrorReturn(t -> new VerifyResponseWithoutKbs(ServiceResponse.forUnknownError(t))) + .doOnSuccess(p -> { + if (p.hasResult()) { + restoreFromStorageService(); + } + }) + .observeOn(AndroidSchedulers.mainThread()); + } + + @WorkerThread + private @NonNull ReRegistrationData verifyReRegisterWithPinInternal(@NonNull String pin) + throws KeyBackupSystemWrongPinException, IOException, KeyBackupSystemNoDataException + { + String localPinHash = SignalStore.kbsValues().getLocalPinHash(); + + if (hasRecoveryPassword() && localPinHash != null && PinHashing.verifyLocalPinHash(localPinHash, pin)) { + Log.i(TAG, "Local pin matches input, attempting registration"); + return ReRegistrationData.canProceed(new KbsPinData(SignalStore.kbsValues().getOrCreateMasterKey(), SignalStore.kbsValues().getRegistrationLockTokenResponse())); + } else { + TokenData data = getKeyBackupCurrentToken(); + if (data == null) { + Log.w(TAG, "No token data, abort skip flow"); + return ReRegistrationData.cannotProceed(); + } + + KbsPinData kbsPinData = KbsRepository.restoreMasterKey(pin, data.getEnclave(), data.getBasicAuth(), data.getTokenResponse()); + if (kbsPinData == null || kbsPinData.getMasterKey() == null) { + Log.w(TAG, "No kbs data, abort skip flow"); + return ReRegistrationData.cannotProceed(); + } + + setRecoveryPassword(kbsPinData.getMasterKey().deriveRegistrationRecoveryPassword()); + return ReRegistrationData.canProceed(kbsPinData); + } + } + + private Single verifyReRegisterWithRecoveryPassword(@NonNull String pin, @NonNull KbsPinData pinData) { + RegistrationData registrationData = getRegistrationData(); + if (registrationData.getRecoveryPassword() == null) { + throw new IllegalStateException("No valid recovery password"); + } + + return verifyAccountRepository.registerAccount(null, registrationData, null, null) + .onErrorReturn(ServiceResponse::forUnknownError) + .map(VerifyResponseWithoutKbs::new) + .flatMap(processor -> { + if (processor.registrationLock()) { + return verifyAccountRepository.registerAccount(null, registrationData, pin, () -> pinData) + .onErrorReturn(ServiceResponse::forUnknownError) + .map(r -> new VerifyResponseWithRegistrationLockProcessor(r, getKeyBackupCurrentToken())); + } else { + return Single.just(processor); + } + }) + .flatMap(processor -> { + if (processor.hasResult()) { + VerifyResponse verifyResponse = processor.getResult(); + boolean setRegistrationLockEnabled = verifyResponse.getKbsData() != null; + + if (!setRegistrationLockEnabled) { + verifyResponse = new VerifyResponse(processor.getResult().getVerifyAccountResponse(), pinData, pin); + } + + return registrationRepository.registerAccount(registrationData, verifyResponse, setRegistrationLockEnabled) + .map(r -> { + return setRegistrationLockEnabled ? new VerifyResponseWithRegistrationLockProcessor(r, getKeyBackupCurrentToken()) + : new VerifyResponseWithoutKbs(r); + }); + } else { + return Single.just(processor); + } + }) + .observeOn(AndroidSchedulers.mainThread()); + } + + public @NonNull Single canEnterSkipSmsFlow() { + return Single.just(hasRecoveryPassword()) + .flatMap(hasRecoveryPassword -> { + if (hasRecoveryPassword) { + Log.d(TAG, "Have valid recovery password but still checking kbs credentials as a backup"); + return checkForValidKbsAuthCredentials().map(unused -> true); + } else { + return checkForValidKbsAuthCredentials(); + } + }); + } + + private Single checkForValidKbsAuthCredentials() { + return registrationRepository.getKbsAuthCredential(getRegistrationData()) + .flatMap(p -> { + if (p.getValid() != null) { + return kbsRepository.getToken(p.getValid()) + .flatMap(r -> { + if (r.getResult().isPresent()) { + setKeyBackupTokenData(r.getResult().get()); + return Single.just(true); + } else { + return Single.just(false); + } + }); + } else { + return Single.just(false); + } + }) + .onErrorReturnItem(false) + .observeOn(AndroidSchedulers.mainThread()); + } + + private void restoreFromStorageService() { + SignalStore.onboarding().clearAll(); + + Stopwatch stopwatch = new Stopwatch("ReRegisterRestore"); + + ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN); + stopwatch.split("AccountRestore"); + + ApplicationDependencies.getJobManager().runSynchronously(new StorageSyncJob(), TimeUnit.SECONDS.toMillis(10)); + stopwatch.split("ContactRestore"); + + try { + FeatureFlags.refreshSync(); + } catch (IOException e) { + Log.w(TAG, "Failed to refresh flags.", e); + } + stopwatch.split("FeatureFlags"); + + stopwatch.stop(TAG); + } + + private boolean hasRecoveryPassword() { + return getRecoveryPassword() != null && Objects.equals(getRegistrationData().getE164(), SignalStore.account().getE164()); + } + + private static class ReRegistrationData { + public boolean canProceed; + public KbsPinData pinData; + + private ReRegistrationData(boolean canProceed, @Nullable KbsPinData pinData) { + this.canProceed = canProceed; + this.pinData = pinData; + } + + public static ReRegistrationData cannotProceed() { + return new ReRegistrationData(false, null); + } + + public static ReRegistrationData canProceed(@NonNull KbsPinData pinData) { + return new ReRegistrationData(true, pinData); + } } public static final class Factory extends AbstractSavedStateViewModelFactory { diff --git a/app/src/main/res/layout/fragment_registration_lock.xml b/app/src/main/res/layout/fragment_registration_lock.xml index 9f130a3f3f..e3be5f4bb7 100644 --- a/app/src/main/res/layout/fragment_registration_lock.xml +++ b/app/src/main/res/layout/fragment_registration_lock.xml @@ -4,8 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:fillViewport="true" - tools:viewBindingIgnore="true"> + android:fillViewport="true"> + + + tools:layout="@layout/fragment_registration_country_picker" /> + + + + + + + + tools:layout="@layout/fragment_registration_captcha" /> > checkBackupAuthCredentials(@Nonnull String e164, @Nonnull List basicAuthTokens) { + List usernamePasswords = basicAuthTokens + .stream() + .limit(10) + .map(t -> { + try { + return new String(Base64.decode(t.replace("Basic ", "").trim()), StandardCharsets.ISO_8859_1); + } catch (IOException e) { + return null; + } + }) + .collect(Collectors.toList()); + + return pushServiceSocket.checkBackupAuthCredentials(new BackupAuthCheckRequest(e164, usernamePasswords), DefaultResponseMapper.getDefault(BackupAuthCheckResponse.class)); + } + /** * Request a push challenge. A number will be pushed to the GCM (FCM) id. This can then be used * during SMS/call requests to bypass the CAPTCHA. @@ -326,44 +347,12 @@ public class SignalServiceAccountManager { /** * Refresh account attributes with server. * - * @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 Only supply if pin has not yet been migrated to KBS. - * @param registrationLock Only supply if found on KBS. - * * @throws IOException */ - public void setAccountAttributes(String signalingKey, - int signalProtocolRegistrationId, - boolean fetchesMessages, - String pin, - String registrationLock, - byte[] unidentifiedAccessKey, - boolean unrestrictedUnidentifiedAccess, - AccountAttributes.Capabilities capabilities, - boolean discoverableByPhoneNumber, - byte[] encryptedDeviceName, - int pniRegistrationId, - String recoveryPassword) + public void setAccountAttributes(@Nonnull AccountAttributes accountAttributes) throws IOException { - this.pushServiceSocket.setAccountAttributes( - signalingKey, - signalProtocolRegistrationId, - fetchesMessages, - pin, - registrationLock, - unidentifiedAccessKey, - unrestrictedUnidentifiedAccess, - capabilities, - discoverableByPhoneNumber, - encryptedDeviceName, - pniRegistrationId, - recoveryPassword - ); + this.pushServiceSocket.setAccountAttributes(accountAttributes); } /** diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckRequest.kt b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckRequest.kt new file mode 100644 index 0000000000..1128b674fd --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/BackupAuthCheckRequest.kt @@ -0,0 +1,46 @@ +package org.whispersystems.signalservice.internal.push + +import com.fasterxml.jackson.annotation.JsonCreator +import okio.ByteString.Companion.encode +import org.whispersystems.signalservice.internal.ServiceResponse +import org.whispersystems.signalservice.internal.ServiceResponseProcessor +import java.nio.charset.StandardCharsets + +/** + * Request body JSON for verifying stored KBS auth credentials. + */ +@Suppress("unused") +class BackupAuthCheckRequest @JsonCreator constructor( + val number: String?, + val passwords: List +) + +/** + * Verify KBS auth credentials JSON response. + */ +data class BackupAuthCheckResponse @JsonCreator constructor( + private val matches: Map> +) { + private val actualMatches = matches["matches"] ?: emptyMap() + + val match: String? = actualMatches.entries.firstOrNull { it.value.toString() == "match" }?.key?.toBasic() + val invalid: List = actualMatches.filterValues { it.toString() == "invalid" }.keys.map { it.toBasic() } + + /** Server expects and returns values as : but we prefer the full encoded Basic auth header format */ + private fun String.toBasic(): String { + return "Basic ${encode(StandardCharsets.ISO_8859_1).base64()}" + } +} + +/** + * Processes a response from the verify stored KBS auth credentials request. + */ +class BackupAuthCheckProcessor(response: ServiceResponse) : ServiceResponseProcessor(response) { + fun getInvalid(): List { + return response.result.map { it.invalid }.orElse(emptyList()) + } + + fun getValid(): String? { + return response.result.map { it.match }.orElse(null) + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index b8454544f2..81c550288e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -286,6 +286,8 @@ public class PushServiceSocket { private static final String REPORT_SPAM = "/v1/messages/report/%s/%s"; + private static final String BACKUP_AUTH_CHECK = "/v1/backup/auth/check"; + private static final String SERVER_DELIVERED_TIMESTAMP_HEADER = "X-Signal-Timestamp"; private static final Map NO_HEADERS = Collections.emptyMap(); @@ -428,39 +430,13 @@ public class PushServiceSocket { return JsonUtil.fromJson(responseBody, VerifyAccountResponse.class); } - public void setAccountAttributes(String signalingKey, - int registrationId, - boolean fetchesMessages, - String pin, - String registrationLock, - byte[] unidentifiedAccessKey, - boolean unrestrictedUnidentifiedAccess, - AccountAttributes.Capabilities capabilities, - boolean discoverableByPhoneNumber, - byte[] encryptedDeviceName, - int pniRegistrationId, - String recoveryPassword) + public void setAccountAttributes(@Nonnull AccountAttributes accountAttributes) throws IOException { - if (registrationLock != null && pin != null) { + if (accountAttributes.getRegistrationLock() != null && accountAttributes.getPin() != null) { throw new AssertionError("Pin should be null if registrationLock is set."); } - String name = (encryptedDeviceName == null) ? null : Base64.encodeBytes(encryptedDeviceName); - - AccountAttributes accountAttributes = new AccountAttributes(signalingKey, - registrationId, - fetchesMessages, - pin, - registrationLock, - unidentifiedAccessKey, - unrestrictedUnidentifiedAccess, - capabilities, - discoverableByPhoneNumber, - name, - pniRegistrationId, - recoveryPassword); - makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes)); } @@ -929,6 +905,22 @@ public class PushServiceSocket { .onErrorReturn(ServiceResponse::forUnknownError); } + public Single> checkBackupAuthCredentials(@Nonnull BackupAuthCheckRequest request, + @Nonnull ResponseMapper responseMapper) + { + Single> requestSingle = Single.fromCallable(() -> { + try (Response response = getServiceConnection(BACKUP_AUTH_CHECK, "POST", jsonRequestBody(JsonUtil.toJson(request)), Collections.emptyMap(), Optional.empty(), false)) { + String body = response.body() != null ? readBodyString(response.body()): ""; + return responseMapper.map(response.code(), body, response::header, false); + } + }); + + return requestSingle + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .onErrorReturn(ServiceResponse::forUnknownError); + } + /** * GET /v1/accounts/username_hash/{usernameHash} *