kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add skip SMS flow.
rodzic
a47e3900c1
commit
4f458a022f
|
@ -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()))
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<RefreshKbsCredentialsJob> {
|
||||
override fun create(parameters: Parameters, data: Data): RefreshKbsCredentialsJob {
|
||||
return RefreshKbsCredentialsJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> tokens) {
|
||||
putList(KBS_AUTH_TOKENS, tokens, StringStringSerializer.INSTANCE);
|
||||
setLastRefreshAuthTimestamp(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public synchronized List<String> getKbsAuthTokenList() {
|
||||
|
@ -202,6 +207,16 @@ public final class KbsValues extends SignalStoreValues {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean removeAuthTokens(@NonNull List<String> invalid) {
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ServiceResponse<VerifyResponse>> registerAccount(@NonNull RegistrationData registrationData,
|
||||
@NonNull VerifyResponse response)
|
||||
@NonNull VerifyResponse response,
|
||||
boolean setRegistrationLockEnabled)
|
||||
{
|
||||
return Single.<ServiceResponse<VerifyResponse>>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<BackupAuthCheckProcessor> 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ServiceResponse<VerifyResponse>> {
|
||||
fun registerAccount(sessionId: String?, registrationData: RegistrationData, pin: String? = null, kbsPinDataProducer: KbsPinDataProducer? = null): Single<ServiceResponse<VerifyResponse>> {
|
||||
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
|
||||
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Void> 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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <T> void setInitialDefaultValue(@NonNull String key, @NonNull T initialValue) {
|
||||
protected <T> 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<Long> getLockedTimeRemaining() {
|
||||
return savedState.getLiveData(STATE_TIME_REMAINING, 0L);
|
||||
}
|
||||
|
|
|
@ -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<VerifyResponseProcessor> onVerifySuccess(@NonNull VerifyResponseProcessor processor) {
|
||||
return registrationRepository.registerAccount(getRegistrationData(), processor.getResult())
|
||||
return registrationRepository.registerAccount(getRegistrationData(), processor.getResult(), false)
|
||||
.map(VerifyResponseWithoutKbs::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Single<VerifyResponseWithRegistrationLockProcessor> 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<VerifyResponseProcessor> 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<VerifyResponseProcessor> 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);
|
||||
}
|
||||
})
|
||||
.<VerifyResponseProcessor>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<Boolean> 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<Boolean> 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 {
|
||||
|
|
|
@ -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">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -110,13 +110,21 @@
|
|||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
|
||||
<action
|
||||
android:id="@+id/action_reRegisterWithPinFragment"
|
||||
app:destination="@id/reRegisterWithPinFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
|
||||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
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" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/enterCodeFragment"
|
||||
|
@ -191,6 +199,31 @@
|
|||
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/reRegisterWithPinFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.ReRegisterWithPinFragment"
|
||||
tools:layout="@layout/fragment_registration_lock">
|
||||
|
||||
<action
|
||||
android:id="@+id/action_reRegisterWithPinFragment_to_enterCodeFragment"
|
||||
app:destination="@id/enterCodeFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@+id/enterPhoneNumberFragment"/>
|
||||
|
||||
<action
|
||||
android:id="@+id/action_reRegisterWithPinFragment_to_registrationCompletePlaceHolderFragment"
|
||||
app:destination="@id/registrationCompletePlaceHolderFragment"
|
||||
app:enterAnim="@anim/nav_default_enter_anim"
|
||||
app:exitAnim="@anim/nav_default_exit_anim"
|
||||
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||
app:popUpTo="@+id/welcomeFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/accountLockedFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.AccountLockedFragment"
|
||||
|
@ -201,7 +234,7 @@
|
|||
android:id="@+id/captchaFragment"
|
||||
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"
|
||||
android:label="fragment_captcha"
|
||||
tools:layout="@layout/fragment_registration_captcha"/>
|
||||
tools:layout="@layout/fragment_registration_captcha" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/restoreBackupFragment"
|
||||
|
|
|
@ -56,6 +56,8 @@ import org.whispersystems.signalservice.internal.ServiceResponse;
|
|||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
import org.whispersystems.signalservice.internal.crypto.PrimaryProvisioningCipher;
|
||||
import org.whispersystems.signalservice.internal.push.AuthCredentials;
|
||||
import org.whispersystems.signalservice.internal.push.BackupAuthCheckRequest;
|
||||
import org.whispersystems.signalservice.internal.push.BackupAuthCheckResponse;
|
||||
import org.whispersystems.signalservice.internal.push.CdsiAuthResponse;
|
||||
import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
|
@ -73,9 +75,11 @@ import org.whispersystems.signalservice.internal.storage.protos.StorageManifest;
|
|||
import org.whispersystems.signalservice.internal.storage.protos.WriteOperation;
|
||||
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.signalservice.internal.websocket.DefaultResponseMapper;
|
||||
import org.whispersystems.util.Base64;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyStore;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
@ -93,6 +97,7 @@ import java.util.concurrent.ExecutionException;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
@ -206,6 +211,22 @@ public class SignalServiceAccountManager {
|
|||
}
|
||||
}
|
||||
|
||||
public Single<ServiceResponse<BackupAuthCheckResponse>> checkBackupAuthCredentials(@Nonnull String e164, @Nonnull List<String> basicAuthTokens) {
|
||||
List<String> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<String>
|
||||
)
|
||||
|
||||
/**
|
||||
* Verify KBS auth credentials JSON response.
|
||||
*/
|
||||
data class BackupAuthCheckResponse @JsonCreator constructor(
|
||||
private val matches: Map<String, Map<String, Any>>
|
||||
) {
|
||||
private val actualMatches = matches["matches"] ?: emptyMap()
|
||||
|
||||
val match: String? = actualMatches.entries.firstOrNull { it.value.toString() == "match" }?.key?.toBasic()
|
||||
val invalid: List<String> = actualMatches.filterValues { it.toString() == "invalid" }.keys.map { it.toBasic() }
|
||||
|
||||
/** Server expects and returns values as <username>:<password> 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<BackupAuthCheckResponse>) : ServiceResponseProcessor<BackupAuthCheckResponse>(response) {
|
||||
fun getInvalid(): List<String> {
|
||||
return response.result.map { it.invalid }.orElse(emptyList())
|
||||
}
|
||||
|
||||
fun getValid(): String? {
|
||||
return response.result.map { it.match }.orElse(null)
|
||||
}
|
||||
}
|
|
@ -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<String, String> 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<ServiceResponse<BackupAuthCheckResponse>> checkBackupAuthCredentials(@Nonnull BackupAuthCheckRequest request,
|
||||
@Nonnull ResponseMapper<BackupAuthCheckResponse> responseMapper)
|
||||
{
|
||||
Single<ServiceResponse<BackupAuthCheckResponse>> 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}
|
||||
*
|
||||
|
|
Ładowanie…
Reference in New Issue