Clarify networking call order during registration flow.

fork-5.53.8
Cody Henthorne 2021-08-31 10:04:40 -04:00 zatwierdzone przez Greyson Parrelli
rodzic a3d72fc06c
commit 8e32592218
43 zmienionych plików z 1600 dodań i 1435 usunięć

Wyświetl plik

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

Wyświetl plik

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.registration.service;
package org.thoughtcrime.securesms.pin;
import androidx.annotation.NonNull;

Wyświetl plik

@ -1,24 +1,15 @@
package org.thoughtcrime.securesms.pin;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.KbsEnclave;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.io.IOException;
import java.util.Objects;
@ -31,52 +22,12 @@ public class PinRestoreRepository {
private final Executor executor = SignalExecutors.UNBOUNDED;
void getToken(@NonNull Callback<Optional<TokenData>> callback) {
executor.execute(() -> {
try {
callback.onComplete(Optional.fromNullable(getTokenSync(null)));
} catch (IOException e) {
callback.onComplete(Optional.absent());
}
});
}
/**
* @param authorization If this is being called before the user is registered (i.e. as part of
* reglock), you must pass in an authorization token that can be used to
* retrieve a backup. Otherwise, pass in null and we'll fetch one.
*/
public @NonNull TokenData getTokenSync(@Nullable String authorization) throws IOException {
TokenData firstKnownTokenData = null;
for (KbsEnclave enclave : KbsEnclaves.all()) {
KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave);
authorization = authorization == null ? kbs.getAuthorization() : authorization;
TokenResponse token = kbs.getToken(authorization);
TokenData tokenData = new TokenData(enclave, authorization, token);
if (tokenData.getTriesRemaining() > 0) {
Log.i(TAG, "Found data! " + enclave.getEnclaveName());
return tokenData;
} else if (firstKnownTokenData == null) {
Log.i(TAG, "No data, but storing as the first response. " + enclave.getEnclaveName());
firstKnownTokenData = tokenData;
} else {
Log.i(TAG, "No data, and we already have a 'first response'. " + enclave.getEnclaveName());
}
}
return Objects.requireNonNull(firstKnownTokenData);
}
void submitPin(@NonNull String pin, @NonNull TokenData tokenData, @NonNull Callback<PinResultData> callback) {
executor.execute(() -> {
try {
Stopwatch stopwatch = new Stopwatch("PinSubmission");
KbsPinData kbsData = PinState.restoreMasterKey(pin, tokenData.getEnclave(), tokenData.getBasicAuth(), tokenData.getTokenResponse());
KbsPinData kbsData = KbsRepository.restoreMasterKey(pin, tokenData.getEnclave(), tokenData.getBasicAuth(), tokenData.getTokenResponse());
PinState.onSignalPinRestore(ApplicationDependencies.getApplication(), Objects.requireNonNull(kbsData), pin);
stopwatch.split("MasterKey");
@ -103,83 +54,6 @@ public class PinRestoreRepository {
void onComplete(@NonNull T value);
}
public static class TokenData implements Parcelable {
private final KbsEnclave enclave;
private final String basicAuth;
private final TokenResponse tokenResponse;
TokenData(@NonNull KbsEnclave enclave, @NonNull String basicAuth, @NonNull TokenResponse tokenResponse) {
this.enclave = enclave;
this.basicAuth = basicAuth;
this.tokenResponse = tokenResponse;
}
private TokenData(Parcel in) {
//noinspection ConstantConditions
this.enclave = new KbsEnclave(in.readString(), in.readString(), in.readString());
this.basicAuth = in.readString();
byte[] backupId = new byte[0];
byte[] token = new byte[0];
in.readByteArray(backupId);
in.readByteArray(token);
this.tokenResponse = new TokenResponse(backupId, token, in.readInt());
}
public static @NonNull TokenData withResponse(@NonNull TokenData data, @NonNull TokenResponse response) {
return new TokenData(data.getEnclave(), data.getBasicAuth(), response);
}
public int getTriesRemaining() {
return tokenResponse.getTries();
}
public @NonNull String getBasicAuth() {
return basicAuth;
}
public @NonNull TokenResponse getTokenResponse() {
return tokenResponse;
}
public @NonNull KbsEnclave getEnclave() {
return enclave;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(enclave.getEnclaveName());
dest.writeString(enclave.getServiceId());
dest.writeString(enclave.getMrEnclave());
dest.writeString(basicAuth);
dest.writeByteArray(tokenResponse.getBackupId());
dest.writeByteArray(tokenResponse.getToken());
dest.writeInt(tokenResponse.getTries());
}
public static final Creator<TokenData> CREATOR = new Creator<TokenData>() {
@Override
public TokenData createFromParcel(Parcel in) {
return new TokenData(in);
}
@Override
public TokenData[] newArray(int size) {
return new TokenData[size];
}
};
}
static class PinResultData {
private final PinResult result;
private final TokenData tokenData;

Wyświetl plik

@ -15,15 +15,17 @@ public class PinRestoreViewModel extends ViewModel {
private final PinRestoreRepository repo;
private final DefaultValueLiveData<TriesRemaining> triesRemaining;
private final SingleLiveEvent<Event> event;
private final KbsRepository kbsRepository;
private volatile PinRestoreRepository.TokenData tokenData;
private volatile TokenData tokenData;
public PinRestoreViewModel() {
this.repo = new PinRestoreRepository();
this.kbsRepository = new KbsRepository();
this.triesRemaining = new DefaultValueLiveData<>(new TriesRemaining(10, false));
this.event = new SingleLiveEvent<>();
repo.getToken(token -> {
kbsRepository.getToken(token -> {
if (token.isPresent()) {
updateTokenData(token.get(), false);
} else {
@ -67,7 +69,7 @@ public class PinRestoreViewModel extends ViewModel {
}
});
} else {
repo.getToken(token -> {
kbsRepository.getToken(token -> {
if (token.isPresent()) {
updateTokenData(token.get(), false);
onPinSubmitted(pin, pinKeyboardType);
@ -86,7 +88,7 @@ public class PinRestoreViewModel extends ViewModel {
return event;
}
private void updateTokenData(@NonNull PinRestoreRepository.TokenData tokenData, boolean incorrectGuess) {
private void updateTokenData(@NonNull TokenData tokenData, boolean incorrectGuess) {
this.tokenData = tokenData;
triesRemaining.postValue(new TriesRemaining(tokenData.getTriesRemaining(), incorrectGuess));
}

Wyświetl plik

@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.lock.PinHashing;
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.guava.Optional;
@ -41,61 +40,6 @@ public final class PinState {
private static final String TAG = Log.tag(PinState.class);
/**
* Invoked during registration to restore the master key based on the server response during
* verification.
*
* Does not affect {@link PinState}.
*/
public static synchronized @Nullable KbsPinData restoreMasterKey(@Nullable String pin,
@NonNull KbsEnclave enclave,
@Nullable String basicStorageCredentials,
@NonNull TokenResponse tokenResponse)
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
{
Log.i(TAG, "restoreMasterKey()");
if (pin == null) return null;
if (basicStorageCredentials == null) {
throw new AssertionError("Cannot restore KBS key, no storage credentials supplied");
}
Log.i(TAG, "Preparing to restore from " + enclave.getEnclaveName());
return restoreMasterKeyFromEnclave(enclave, pin, basicStorageCredentials, tokenResponse);
}
private static @NonNull KbsPinData restoreMasterKeyFromEnclave(@NonNull KbsEnclave enclave,
@NonNull String pin,
@NonNull String basicStorageCredentials,
@NonNull TokenResponse tokenResponse)
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
{
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(enclave);
KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(basicStorageCredentials, tokenResponse);
try {
Log.i(TAG, "Restoring pin from KBS");
HashedPin hashedPin = PinHashing.hashPin(pin, session);
KbsPinData kbsData = session.restorePin(hashedPin);
if (kbsData != null) {
Log.i(TAG, "Found registration lock token on KBS.");
} else {
throw new AssertionError("Null not expected");
}
return kbsData;
} catch (UnauthenticatedResponseException | InvalidKeyException e) {
Log.w(TAG, "Failed to restore key", e);
throw new IOException(e);
} catch (KeyBackupServicePinException e) {
Log.w(TAG, "Incorrect pin", e);
throw new KeyBackupSystemWrongPinException(e.getToken());
}
}
/**
* Invoked after a user has successfully registered. Ensures all the necessary state is updated.
*/

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.registration
import org.signal.zkgroup.profiles.ProfileKey
data class RegistrationData(
val code: String,
val e164: String,
val password: String,
val registrationId: Int,
val profileKey: ProfileKey,
val fcmToken: String?
) {
val isFcm: Boolean = fcmToken != null
val isNotFcm: Boolean = fcmToken == null
}

Wyświetl plik

@ -10,6 +10,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.lifecycle.ViewModelProviders;
import com.google.android.gms.auth.api.phone.SmsRetriever;
import com.google.android.gms.common.api.CommonStatusCodes;
@ -18,6 +19,7 @@ import com.google.android.gms.common.api.Status;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.service.VerificationCodeParser;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.whispersystems.libsignal.util.guava.Optional;
@ -28,10 +30,9 @@ public final class RegistrationNavigationActivity extends AppCompatActivity {
public static final String RE_REGISTRATION_EXTRA = "re_registration";
private SmsRetrieverReceiver smsRetrieverReceiver;
private SmsRetrieverReceiver smsRetrieverReceiver;
private RegistrationViewModel viewModel;
/**
*/
public static Intent newIntentForNewRegistration(@NonNull Context context, @Nullable Intent originalIntent) {
Intent intent = new Intent(context, RegistrationNavigationActivity.class);
intent.putExtra(RE_REGISTRATION_EXTRA, false);
@ -58,6 +59,8 @@ public final class RegistrationNavigationActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = ViewModelProviders.of(this, new RegistrationViewModel.Factory(this, isReregister(getIntent()))).get(RegistrationViewModel.class);
setContentView(R.layout.activity_registration_navigation);
initializeChallengeListener();
@ -73,6 +76,8 @@ public final class RegistrationNavigationActivity extends AppCompatActivity {
if (intent.getData() != null) {
CommunicationActions.handlePotentialProxyLinkUrl(this, intent.getDataString());
}
viewModel.setIsReregister(isReregister(intent));
}
@Override
@ -81,6 +86,10 @@ public final class RegistrationNavigationActivity extends AppCompatActivity {
shutdownChallengeListener();
}
private boolean isReregister(@NonNull Intent intent) {
return intent.getBooleanExtra(RE_REGISTRATION_EXTRA, false);
}
private void initializeChallengeListener() {
smsRetrieverReceiver = new SmsRetrieverReceiver();

Wyświetl plik

@ -0,0 +1,185 @@
package org.thoughtcrime.securesms.registration;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
import org.thoughtcrime.securesms.pin.PinState;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
/**
* Operations required for finalizing the registration of an account. This is
* to be used after verifying the code and registration lock (if necessary) with
* the server and being issued a UUID.
*/
public final class RegistrationRepository {
private static final String TAG = Log.tag(RegistrationRepository.class);
private final Application context;
public RegistrationRepository(@NonNull Application context) {
this.context = context;
}
public int getRegistrationId() {
int registrationId = TextSecurePreferences.getLocalRegistrationId(context);
if (registrationId == 0) {
registrationId = KeyHelper.generateRegistrationId(false);
TextSecurePreferences.setLocalRegistrationId(context, registrationId);
}
return registrationId;
}
public @NonNull ProfileKey getProfileKey(@NonNull String e164) {
ProfileKey profileKey = findExistingProfileKey(context, e164);
if (profileKey == null) {
profileKey = ProfileKeyUtil.createNew();
Log.i(TAG, "No profile key found, created a new one");
}
return profileKey;
}
public Single<ServiceResponse<VerifyAccountResponse>> registerAccountWithoutRegistrationLock(@NonNull RegistrationData registrationData,
@NonNull VerifyAccountResponse response)
{
return registerAccount(registrationData, response, null, null);
}
public Single<ServiceResponse<VerifyAccountResponse>> registerAccountWithRegistrationLock(@NonNull RegistrationData registrationData,
@NonNull VerifyAccountWithRegistrationLockResponse response,
@NonNull String pin)
{
return registerAccount(registrationData, response.getVerifyAccountResponse(), pin, response.getKbsData());
}
private Single<ServiceResponse<VerifyAccountResponse>> registerAccount(@NonNull RegistrationData registrationData,
@NonNull VerifyAccountResponse response,
@Nullable String pin,
@Nullable KbsPinData kbsData)
{
return Single.<ServiceResponse<VerifyAccountResponse>>fromCallable(() -> {
try {
registerAccountInternal(registrationData, response, pin, kbsData);
JobManager jobManager = ApplicationDependencies.getJobManager();
jobManager.add(new DirectoryRefreshJob(false));
jobManager.add(new RotateCertificateJob());
DirectoryRefreshListener.schedule(context);
RotateSignedPreKeyListener.schedule(context);
return ServiceResponse.forResult(response, 200, null);
} catch (IOException e) {
return ServiceResponse.forUnknownError(e);
}
}).subscribeOn(Schedulers.io());
}
@WorkerThread
private void registerAccountInternal(@NonNull RegistrationData registrationData,
@NonNull VerifyAccountResponse response,
@Nullable String pin,
@Nullable KbsPinData kbsData)
throws IOException
{
SessionUtil.archiveAllSessions();
SenderKeyUtil.clearAllState(context);
UUID uuid = UuidUtil.parseOrThrow(response.getUuid());
boolean hasPin = response.isStorageCapable();
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);
SignalServiceAccountManager accountManager = AccountManagerFactory.createAuthenticated(context, uuid, registrationData.getE164(), registrationData.getPassword());
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
if (registrationData.isFcm()) {
accountManager.setGcmId(Optional.fromNullable(registrationData.getFcmToken()));
}
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
RecipientId selfId = Recipient.externalPush(context, uuid, registrationData.getE164(), true).getId();
recipientDatabase.setProfileSharing(selfId, true);
recipientDatabase.markRegisteredOrThrow(selfId, uuid);
TextSecurePreferences.setLocalNumber(context, registrationData.getE164());
TextSecurePreferences.setLocalUuid(context, uuid);
recipientDatabase.setProfileKey(selfId, registrationData.getProfileKey());
ApplicationDependencies.getRecipientCache().clearSelf();
TextSecurePreferences.setFcmToken(context, registrationData.getFcmToken());
TextSecurePreferences.setFcmDisabled(context, registrationData.isNotFcm());
TextSecurePreferences.setWebsocketRegistered(context, true);
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(selfId,
identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true);
TextSecurePreferences.setPushRegistered(context, true);
TextSecurePreferences.setPushServerPassword(context, registrationData.getPassword());
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
TextSecurePreferences.setPromptedPushRegistration(context, true);
TextSecurePreferences.setUnauthorizedReceived(context, false);
PinState.onRegistration(context, kbsData, pin, hasPin);
}
@WorkerThread
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Optional<RecipientId> recipient = recipientDatabase.getByE164(e164number);
if (recipient.isPresent()) {
return ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).getProfileKey());
}
return null;
}
}

Wyświetl plik

@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.registration
import org.whispersystems.signalservice.api.push.exceptions.LocalRateLimitException
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse
/**
* Process responses from requesting an SMS or Phone code from the server.
*/
class RequestVerificationCodeResponseProcessor(response: ServiceResponse<RequestVerificationCodeResponse>) : ServiceResponseProcessor<RequestVerificationCodeResponse>(response) {
public override fun captchaRequired(): Boolean {
return super.captchaRequired()
}
public override fun rateLimit(): Boolean {
return super.rateLimit()
}
public override fun getError(): Throwable? {
return super.getError()
}
fun localRateLimit(): Boolean {
return error is LocalRateLimitException
}
companion object {
@JvmStatic
fun forLocalRateLimit(): RequestVerificationCodeResponseProcessor {
val response: ServiceResponse<RequestVerificationCodeResponse> = ServiceResponse.forExecutionError(LocalRateLimitException())
return RequestVerificationCodeResponseProcessor(response)
}
}
}

Wyświetl plik

@ -0,0 +1,131 @@
package org.thoughtcrime.securesms.registration
import android.app.Application
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.AppCapabilities
import org.thoughtcrime.securesms.gcm.FcmUtil
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.KbsRepository
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
import org.thoughtcrime.securesms.pin.TokenData
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.KbsPinData
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.util.Locale
import java.util.concurrent.TimeUnit
/**
* Request SMS/Phone verification codes to help prove ownership of a phone number.
*/
class VerifyAccountRepository(private val context: Application) {
fun requestVerificationCode(
e164: String,
password: String,
mode: Mode,
captchaToken: String? = null
): Single<ServiceResponse<RequestVerificationCodeResponse>> {
Log.d(TAG, "SMS Verification requested")
return Single.fromCallable {
val fcmToken: Optional<String> = FcmUtil.getToken()
val accountManager = AccountManagerFactory.createUnauthenticated(context, e164, password)
val pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, fcmToken, e164, PUSH_REQUEST_TIMEOUT)
if (mode == Mode.PHONE_CALL) {
accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.fromNullable(captchaToken), pushChallenge, fcmToken)
} else {
accountManager.requestSmsVerificationCode(mode.isSmsRetrieverSupported, Optional.fromNullable(captchaToken), pushChallenge, fcmToken)
}
}.subscribeOn(Schedulers.io())
}
fun verifyAccount(registrationData: RegistrationData): Single<ServiceResponse<VerifyAccountResponse>> {
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated(
context,
registrationData.e164,
registrationData.password
)
return Single.fromCallable {
accountManager.verifyAccount(
registrationData.code,
registrationData.registrationId,
registrationData.isNotFcm,
unidentifiedAccessKey,
universalUnidentifiedAccess,
AppCapabilities.getCapabilities(true),
SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable
)
}.subscribeOn(Schedulers.io())
}
fun verifyAccountWithPin(registrationData: RegistrationData, pin: String, tokenData: TokenData): Single<ServiceResponse<VerifyAccountWithRegistrationLockResponse>> {
val universalUnidentifiedAccess: Boolean = TextSecurePreferences.isUniversalUnidentifiedAccess(context)
val unidentifiedAccessKey: ByteArray = UnidentifiedAccess.deriveAccessKeyFrom(registrationData.profileKey)
val accountManager: SignalServiceAccountManager = AccountManagerFactory.createUnauthenticated(
context,
registrationData.e164,
registrationData.password
)
return Single.fromCallable {
try {
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
val registrationLockV2: String = kbsData.masterKey.deriveRegistrationLock()
val response: ServiceResponse<VerifyAccountResponse> = accountManager.verifyAccountWithRegistrationLockPin(
registrationData.code,
registrationData.registrationId,
registrationData.isNotFcm,
registrationLockV2,
unidentifiedAccessKey,
universalUnidentifiedAccess,
AppCapabilities.getCapabilities(true),
SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isDiscoverable
)
VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
} catch (e: KeyBackupSystemWrongPinException) {
ServiceResponse.forExecutionError(e)
} catch (e: KeyBackupSystemNoDataException) {
ServiceResponse.forExecutionError(e)
}
}.subscribeOn(Schedulers.io())
}
enum class Mode(val isSmsRetrieverSupported: Boolean) {
SMS_WITH_LISTENER(true),
SMS_WITHOUT_LISTENER(false),
PHONE_CALL(false);
}
companion object {
private val TAG = Log.tag(VerifyAccountRepository::class.java)
private val PUSH_REQUEST_TIMEOUT = TimeUnit.SECONDS.toMillis(5)
}
data class VerifyAccountWithRegistrationLockResponse(val verifyAccountResponse: VerifyAccountResponse, val kbsData: KbsPinData) {
companion object {
fun from(response: ServiceResponse<VerifyAccountResponse>, kbsData: KbsPinData): ServiceResponse<VerifyAccountWithRegistrationLockResponse> {
return if (response.result.isPresent) {
ServiceResponse.forResult(VerifyAccountWithRegistrationLockResponse(response.result.get(), kbsData), 200, null)
} else {
ServiceResponse.coerceError(response)
}
}
}
}
}

Wyświetl plik

@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.registration
import org.thoughtcrime.securesms.pin.TokenData
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
import org.whispersystems.signalservice.internal.push.LockedException
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
/**
* Process responses from attempting to verify an account for use in account registration.
*/
sealed class VerifyAccountResponseProcessor(response: ServiceResponse<VerifyAccountResponse>) : ServiceResponseProcessor<VerifyAccountResponse>(response) {
open val tokenData: TokenData? = null
public override fun authorizationFailed(): Boolean {
return super.authorizationFailed()
}
public override fun registrationLock(): Boolean {
return super.registrationLock()
}
public override fun rateLimit(): Boolean {
return super.rateLimit()
}
public override fun getError(): Throwable? {
return super.getError()
}
fun getLockedException(): LockedException {
return error as LockedException
}
abstract fun isKbsLocked(): Boolean
}
/**
* Verify processor specific to verifying without needing to handle registration lock.
*/
class VerifyAccountResponseWithoutKbs(response: ServiceResponse<VerifyAccountResponse>) : VerifyAccountResponseProcessor(response) {
override fun isKbsLocked(): Boolean {
return registrationLock() && getLockedException().basicStorageCredentials == null
}
}
/**
* Verify processor specific to verifying and successfully retrieving KBS information to
* later attempt to verif with registration lock data (pin).
*/
class VerifyAccountResponseWithSuccessfulKbs(
response: ServiceResponse<VerifyAccountResponse>,
override val tokenData: TokenData
) : VerifyAccountResponseProcessor(response) {
override fun isKbsLocked(): Boolean {
return registrationLock() && tokenData.triesRemaining == 0
}
}
/**
* Verify processor specific to verifying and unsuccessfully retrieving KBS information that
* is required for attempting to verify a registration locked account.
*/
class VerifyAccountResponseWithFailedKbs(response: ServiceResponse<TokenData>) : VerifyAccountResponseProcessor(ServiceResponse.coerceError(response)) {
override fun isKbsLocked(): Boolean {
return false
}
}

Wyświetl plik

@ -0,0 +1,48 @@
package org.thoughtcrime.securesms.registration
import org.thoughtcrime.securesms.pin.KeyBackupSystemWrongPinException
import org.thoughtcrime.securesms.pin.TokenData
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
/**
* Process responses from attempting to verify an account with registration lock for use in
* account registration.
*/
class VerifyCodeWithRegistrationLockResponseProcessor(
response: ServiceResponse<VerifyAccountWithRegistrationLockResponse>,
val token: TokenData
) : ServiceResponseProcessor<VerifyAccountWithRegistrationLockResponse>(response) {
public override fun rateLimit(): Boolean {
return super.rateLimit()
}
public override fun getError(): Throwable? {
return super.getError()
}
fun wrongPin(): Boolean {
return error is KeyBackupSystemWrongPinException
}
fun getTokenResponse(): TokenResponse {
return (error as KeyBackupSystemWrongPinException).tokenResponse
}
fun isKbsLocked(): Boolean {
return error is KeyBackupSystemNoDataException
}
fun updatedIfRegistrationFailed(response: ServiceResponse<VerifyAccountResponse>): VerifyCodeWithRegistrationLockResponseProcessor {
if (response.result.isPresent) {
return this
}
return VerifyCodeWithRegistrationLockResponseProcessor(ServiceResponse.coerceError(response), token)
}
}

Wyświetl plik

@ -11,12 +11,15 @@ import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import java.util.concurrent.TimeUnit;
public class AccountLockedFragment extends BaseRegistrationFragment {
public class AccountLockedFragment extends LoggingFragment {
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
@ -29,7 +32,8 @@ public class AccountLockedFragment extends BaseRegistrationFragment {
TextView description = view.findViewById(R.id.account_locked_description);
getModel().getLockedTimeRemaining().observe(getViewLifecycleOwner(),
RegistrationViewModel viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
viewModel.getLockedTimeRemaining().observe(getViewLifecycleOwner(),
t -> description.setText(getString(R.string.AccountLockedFragment__your_account_has_been_locked_to_protect_your_privacy, durationToDays(t)))
);

Wyświetl plik

@ -1,159 +0,0 @@
package org.thoughtcrime.securesms.registration.fragments;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.SavedStateViewModelFactory;
import androidx.lifecycle.ViewModelProviders;
import com.dd.CircularProgressButton;
import org.signal.core.util.TranslationDetection;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.SpanUtil;
import java.util.Locale;
import static org.thoughtcrime.securesms.registration.RegistrationNavigationActivity.RE_REGISTRATION_EXTRA;
abstract class BaseRegistrationFragment extends LoggingFragment {
private static final String TAG = Log.tag(BaseRegistrationFragment.class);
private RegistrationViewModel model;
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
this.model = getRegistrationViewModel(requireActivity());
}
protected @NonNull RegistrationViewModel getModel() {
return model;
}
protected boolean isReregister() {
Activity activity = getActivity();
if (activity == null) {
return false;
}
return activity.getIntent().getBooleanExtra(RE_REGISTRATION_EXTRA, false);
}
protected static RegistrationViewModel getRegistrationViewModel(@NonNull FragmentActivity activity) {
SavedStateViewModelFactory savedStateViewModelFactory = new SavedStateViewModelFactory(activity.getApplication(), activity);
return ViewModelProviders.of(activity, savedStateViewModelFactory).get(RegistrationViewModel.class);
}
protected static void setSpinning(@Nullable CircularProgressButton button) {
if (button != null) {
button.setClickable(false);
button.setIndeterminateProgressMode(true);
button.setProgress(50);
}
}
protected static void cancelSpinning(@Nullable CircularProgressButton button) {
if (button != null) {
button.setProgress(0);
button.setIndeterminateProgressMode(false);
button.setClickable(true);
}
}
protected static void hideKeyboard(@NonNull Context context, @NonNull View view) {
InputMethodManager imm = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
/**
* Sets view up to allow log submitting after multiple taps.
*/
protected static void setDebugLogSubmitMultiTapView(@Nullable View view) {
if (view == null) return;
view.setOnClickListener(new View.OnClickListener() {
private static final int DEBUG_TAP_TARGET = 8;
private static final int DEBUG_TAP_ANNOUNCE = 4;
private int debugTapCounter;
@Override
public void onClick(View v) {
Context context = v.getContext();
debugTapCounter++;
if (debugTapCounter >= DEBUG_TAP_TARGET) {
context.startActivity(new Intent(context, SubmitDebugLogActivity.class));
} else if (debugTapCounter >= DEBUG_TAP_ANNOUNCE) {
int remaining = DEBUG_TAP_TARGET - debugTapCounter;
Toast.makeText(context, context.getResources().getQuantityString(R.plurals.RegistrationActivity_debug_log_hint, remaining, remaining), Toast.LENGTH_SHORT).show();
}
}
});
}
/**
* Presents a prompt for the user to confirm their number as long as it can be shown in one of their device languages.
*/
protected final void showConfirmNumberDialogIfTranslated(@NonNull Context context,
@StringRes int firstMessageLine,
@NonNull String e164number,
@NonNull Runnable onConfirmed,
@NonNull Runnable onEditNumber)
{
TranslationDetection translationDetection = new TranslationDetection(context);
if (translationDetection.textExistsInUsersLanguage(firstMessageLine) &&
translationDetection.textExistsInUsersLanguage(R.string.RegistrationActivity_is_your_phone_number_above_correct) &&
translationDetection.textExistsInUsersLanguage(R.string.RegistrationActivity_edit_number))
{
CharSequence message = new SpannableStringBuilder().append(context.getString(firstMessageLine))
.append("\n\n")
.append(SpanUtil.bold(PhoneNumberFormatter.prettyPrint(e164number)))
.append("\n\n")
.append(context.getString(R.string.RegistrationActivity_is_your_phone_number_above_correct));
Log.i(TAG, "Showing confirm number dialog (" + context.getString(firstMessageLine) + ")");
new AlertDialog.Builder(context)
.setMessage(message)
.setPositiveButton(android.R.string.ok,
(a, b) -> {
Log.i(TAG, "Number confirmed");
onConfirmed.run();
})
.setNegativeButton(R.string.RegistrationActivity_edit_number,
(a, b) -> {
Log.i(TAG, "User requested edit number from confirm dialog");
onEditNumber.run();
})
.show();
} else {
Log.i(TAG, "Confirm number dialog not translated in " + Locale.getDefault() + " skipping");
onConfirmed.run();
}
}
}

Wyświetl plik

@ -10,14 +10,20 @@ import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
/**
* Fragment that displays a Captcha in a WebView.
*/
public final class CaptchaFragment extends BaseRegistrationFragment {
public final class CaptchaFragment extends LoggingFragment {
private RegistrationViewModel viewModel;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -46,11 +52,12 @@ public final class CaptchaFragment extends BaseRegistrationFragment {
});
webView.loadUrl(RegistrationConstants.SIGNAL_CAPTCHA_URL);
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
}
private void handleToken(@NonNull String token) {
getModel().onCaptchaResponse(token);
Navigation.findNavController(requireView()).navigate(CaptchaFragmentDirections.actionCaptchaComplete());
viewModel.setCaptchaResponse(token);
NavHostFragment.findNavController(this).navigateUp();
}
}

Wyświetl plik

@ -20,20 +20,18 @@ import androidx.core.text.HtmlCompat;
import androidx.navigation.Navigation;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.documents.Document;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.BackupUtil;
public class ChooseBackupFragment extends BaseRegistrationFragment {
public class ChooseBackupFragment extends LoggingFragment {
private static final String TAG = Log.tag(ChooseBackupFragment.class);
private static final short OPEN_FILE_REQUEST_CODE = 3862;
private View chooseBackupButton;
private TextView learnMore;
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@ -44,10 +42,10 @@ public class ChooseBackupFragment extends BaseRegistrationFragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button);
View chooseBackupButton = view.findViewById(R.id.choose_backup_fragment_button);
chooseBackupButton.setOnClickListener(this::onChooseBackupSelected);
learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more);
TextView learnMore = view.findViewById(R.id.choose_backup_fragment_learn_more);
learnMore.setText(HtmlCompat.fromHtml(String.format("<a href=\"%s\">%s</a>", getString(R.string.backup_support_url), getString(R.string.ChooseBackupFragment__learn_more)), 0));
learnMore.setMovementMethod(LinkMovementMethod.getInstance());
}

Wyświetl plik

@ -14,9 +14,11 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.ListFragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.loaders.CountryListLoader;
@ -39,7 +41,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
model = BaseRegistrationFragment.getRegistrationViewModel(requireActivity());
model = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
countryFilter = view.findViewById(R.id.country_search);
@ -56,7 +58,7 @@ public final class CountryPickerFragment extends ListFragment implements LoaderM
model.onCountrySelected(countryName, countryCode);
Navigation.findNavController(view).navigate(CountryPickerFragmentDirections.actionCountrySelected());
NavHostFragment.findNavController(this).navigateUp();
}
@Override

Wyświetl plik

@ -1,5 +1,8 @@
package org.thoughtcrime.securesms.registration.fragments;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated;
import android.animation.Animator;
import android.os.Bundle;
import android.telephony.PhoneStateListener;
@ -12,38 +15,42 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.navigation.NavController;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.registration.CallMeCountDownView;
import org.thoughtcrime.securesms.components.registration.VerificationCodeView;
import org.thoughtcrime.securesms.components.registration.VerificationPinKeyboard;
import org.thoughtcrime.securesms.pin.PinRestoreRepository;
import org.thoughtcrime.securesms.registration.ReceivedSmsEvent;
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.internal.push.LockedException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class EnterCodeFragment extends BaseRegistrationFragment
implements SignalStrengthPhoneStateListener.Callback
{
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
public final class EnterCodeFragment extends LoggingFragment implements SignalStrengthPhoneStateListener.Callback {
private static final String TAG = Log.tag(EnterCodeFragment.class);
@ -57,7 +64,10 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
private View serviceWarning;
private boolean autoCompleting;
private PhoneStateListener signalStrengthListener;
private PhoneStateListener signalStrengthListener;
private RegistrationViewModel viewModel;
private final LifecycleDisposable disposables = new LifecycleDisposable();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -82,7 +92,7 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
signalStrengthListener = new SignalStrengthPhoneStateListener(this, this);
connectKeyboard(verificationCodeView, keyboard);
hideKeyboard(requireContext(), view);
ViewUtil.hideKeyboard(requireContext(), view);
setOnCodeFullyEnteredListener(verificationCodeView);
@ -99,15 +109,16 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
noCodeReceivedHelp.setOnClickListener(v -> sendEmailToSupport());
RegistrationViewModel model = getModel();
model.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
viewModel.getSuccessfulCodeRequestAttempts().observe(getViewLifecycleOwner(), (attempts) -> {
if (attempts >= 3) {
noCodeReceivedHelp.setVisibility(View.VISIBLE);
scrollView.postDelayed(() -> scrollView.smoothScrollTo(0, noCodeReceivedHelp.getBottom()), 15000);
}
});
model.onStartEnterCode();
viewModel.onStartEnterCode();
}
private void onWrongNumber() {
@ -117,115 +128,112 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
private void setOnCodeFullyEnteredListener(VerificationCodeView verificationCodeView) {
verificationCodeView.setOnCompleteListener(code -> {
RegistrationViewModel model = getModel();
model.onVerificationCodeEntered(code);
callMeCountDown.setVisibility(View.INVISIBLE);
wrongNumber.setVisibility(View.INVISIBLE);
keyboard.displayProgress();
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
Disposable verify = viewModel.verifyCodeAndRegisterAccountWithoutRegistrationLock(code)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(processor -> {
if (processor.hasResult()) {
handleSuccessfulRegistration();
} else if (processor.rateLimit()) {
handleRateLimited();
} else if (processor.registrationLock() && !processor.isKbsLocked()) {
LockedException lockedException = processor.getLockedException();
handleRegistrationLock(lockedException.getTimeRemaining());
} else if (processor.isKbsLocked()) {
handleKbsAccountLocked();
} else if (processor.authorizationFailed()) {
handleIncorrectCodeError();
} else {
Log.w(TAG, "Unable to verify code", processor.getError());
handleGeneralError();
}
});
registrationService.verifyAccount(requireActivity(), model.getFcmToken(), code, null, null,
new CodeVerificationRequest.VerifyCallback() {
@Override
public void onSuccessfulRegistration() {
SimpleTask.run(() -> {
long startTime = System.currentTimeMillis();
try {
FeatureFlags.refreshSync();
Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to get feature flags.");
} catch (IOException e) {
Log.w(TAG, "Failed to refresh flags after " + (System.currentTimeMillis() - startTime) + " ms.", e);
}
return null;
}, none -> {
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
handleSuccessfulRegistration();
}
});
});
}
@Override
public void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining) {
model.setLockedTimeRemaining(timeRemaining);
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
Navigation.findNavController(requireView())
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, true));
}
});
}
@Override
public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull PinRestoreRepository.TokenData tokenData, @NonNull String kbsStorageCredentials) {
model.setLockedTimeRemaining(timeRemaining);
model.setKeyBackupTokenData(tokenData);
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
Navigation.findNavController(requireView())
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, false));
}
});
}
@Override
public void onIncorrectKbsRegistrationLockPin(@NonNull PinRestoreRepository.TokenData tokenData) {
throw new AssertionError("Unexpected, user has made no pin guesses");
}
@Override
public void onRateLimited() {
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationActivity_too_many_attempts)
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
callMeCountDown.setVisibility(View.VISIBLE);
wrongNumber.setVisibility(View.VISIBLE);
verificationCodeView.clear();
keyboard.displayKeyboard();
})
.show();
}
});
}
@Override
public void onKbsAccountLocked(@Nullable Long timeRemaining) {
if (timeRemaining != null) {
model.setLockedTimeRemaining(timeRemaining);
}
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked());
}
@Override
public void onError() {
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
callMeCountDown.setVisibility(View.VISIBLE);
wrongNumber.setVisibility(View.VISIBLE);
verificationCodeView.clear();
keyboard.displayKeyboard();
}
});
}
});
disposables.add(verify);
});
}
private void handleSuccessfulRegistration() {
Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration());
public void handleSuccessfulRegistration() {
SimpleTask.run(() -> {
long startTime = System.currentTimeMillis();
try {
FeatureFlags.refreshSync();
Log.i(TAG, "Took " + (System.currentTimeMillis() - startTime) + " ms to get feature flags.");
} catch (IOException e) {
Log.w(TAG, "Failed to refresh flags after " + (System.currentTimeMillis() - startTime) + " ms.", e);
}
return null;
}, none -> {
keyboard.displaySuccess().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
Navigation.findNavController(requireView()).navigate(EnterCodeFragmentDirections.actionSuccessfulRegistration());
}
});
});
}
public void handleRateLimited() {
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
builder.setTitle(R.string.RegistrationActivity_too_many_attempts)
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_attempts_please_try_again_later)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
callMeCountDown.setVisibility(View.VISIBLE);
wrongNumber.setVisibility(View.VISIBLE);
verificationCodeView.clear();
keyboard.displayKeyboard();
})
.show();
}
});
}
public void handleRegistrationLock(long timeRemaining) {
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
Navigation.findNavController(requireView())
.navigate(EnterCodeFragmentDirections.actionRequireKbsLockPin(timeRemaining, false));
}
});
}
public void handleKbsAccountLocked() {
Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionAccountLocked());
}
public void handleIncorrectCodeError() {
Toast.makeText(requireContext(), R.string.RegistrationActivity_incorrect_code, Toast.LENGTH_LONG).show();
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
callMeCountDown.setVisibility(View.VISIBLE);
wrongNumber.setVisibility(View.VISIBLE);
verificationCodeView.clear();
keyboard.displayKeyboard();
}
});
}
public void handleGeneralError() {
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
keyboard.displayFailure().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
callMeCountDown.setVisibility(View.VISIBLE);
wrongNumber.setVisibility(View.VISIBLE);
verificationCodeView.clear();
keyboard.displayKeyboard();
}
});
}
@Override
@ -283,46 +291,28 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
private void handlePhoneCallRequest() {
showConfirmNumberDialogIfTranslated(requireContext(),
R.string.RegistrationActivity_you_will_receive_a_call_to_verify_this_number,
getModel().getNumber().getE164Number(),
viewModel.getNumber().getE164Number(),
this::handlePhoneCallRequestAfterConfirm,
this::onWrongNumber);
}
private void handlePhoneCallRequestAfterConfirm() {
RegistrationViewModel model = getModel();
String captcha = model.getCaptchaToken();
model.clearCaptchaResponse();
Disposable request = viewModel.requestVerificationCode(VerifyAccountRepository.Mode.PHONE_CALL)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(processor -> {
if (processor.hasResult()) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_call_requested, Toast.LENGTH_LONG).show();
} else if (processor.captchaRequired()) {
NavHostFragment.findNavController(this).navigate(EnterCodeFragmentDirections.actionRequestCaptcha());
} else if (processor.rateLimit()) {
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show();
} else {
Log.w(TAG, "Unable to request phone code", processor.getError());
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
}
});
model.onCallRequested();
NavController navController = Navigation.findNavController(callMeCountDown);
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
registrationService.requestVerificationCode(requireActivity(), RegistrationCodeRequest.Mode.PHONE_CALL, captcha,
new RegistrationCodeRequest.SmsVerificationCodeCallback() {
@Override
public void onNeedCaptcha() {
navController.navigate(EnterCodeFragmentDirections.actionRequestCaptcha());
}
@Override
public void requestSent(@Nullable String fcmToken) {
model.setFcmToken(fcmToken);
model.markASuccessfulAttempt();
}
@Override
public void onRateLimited() {
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show();
}
@Override
public void onError() {
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
}
});
disposables.add(request);
}
private void connectKeyboard(VerificationCodeView verificationCodeView, VerificationPinKeyboard keyboard) {
@ -341,10 +331,9 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
public void onResume() {
super.onResume();
RegistrationViewModel model = getModel();
model.getLiveNumber().observe(getViewLifecycleOwner(), (s) -> header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, s.getFullFormattedNumber())));
header.setText(requireContext().getString(R.string.RegistrationActivity_enter_the_code_we_sent_to_s, viewModel.getNumber().getFullFormattedNumber()));
model.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> callMeCountDown.startCountDownTo(callAtTime));
viewModel.getCanCallAtTime().observe(getViewLifecycleOwner(), callAtTime -> callMeCountDown.startCountDownTo(callAtTime));
}
private void sendEmailToSupport() {
@ -387,8 +376,11 @@ public final class EnterCodeFragment extends BaseRegistrationFragment
@Override public void onAnimationEnd(Animator animation) {
serviceWarning.setVisibility(View.GONE);
}
@Override public void onAnimationStart(Animator animation) {}
@Override public void onAnimationCancel(Animator animation) {}
@Override public void onAnimationRepeat(Animator animation) {}
})
.start();

Wyświetl plik

@ -1,5 +1,10 @@
package org.thoughtcrime.securesms.registration.fragments;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.showConfirmNumberDialogIfTranslated;
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning;
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
@ -22,11 +27,12 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
import com.dd.CircularProgressButton;
import com.google.android.gms.auth.api.phone.SmsRetriever;
@ -34,20 +40,28 @@ import com.google.android.gms.auth.api.phone.SmsRetrieverClient;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.tasks.Task;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.i18n.phonenumbers.AsYouTypeFormatter;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.LabeledEditText;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.PlayServicesUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
public final class EnterPhoneNumberFragment extends LoggingFragment {
private static final String TAG = Log.tag(EnterPhoneNumberFragment.class);
@ -59,6 +73,9 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
private Spinner countrySpinner;
private View cancel;
private ScrollView scrollView;
private RegistrationViewModel viewModel;
private final LifecycleDisposable disposables = new LifecycleDisposable();
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -67,8 +84,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_enter_phone_number, container, false);
}
@ -91,21 +107,23 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
register.setOnClickListener(v -> handleRegister(requireContext()));
if (isReregister()) {
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
if (viewModel.isReregister()) {
cancel.setVisibility(View.VISIBLE);
cancel.setOnClickListener(v -> Navigation.findNavController(v).navigateUp());
} else {
cancel.setVisibility(View.GONE);
}
RegistrationViewModel model = getModel();
NumberViewState number = model.getNumber();
NumberViewState number = viewModel.getNumber();
initNumber(number);
countryCode.getInput().addTextChangedListener(new CountryCodeChangedListener());
if (model.hasCaptchaToken()) {
if (viewModel.hasCaptchaToken()) {
handleRegister(requireContext());
}
@ -145,7 +163,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
numberInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
numberInput.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
hideKeyboard(requireContext(), v);
ViewUtil.hideKeyboard(requireContext(), v);
handleRegister(requireContext());
return true;
}
@ -164,31 +182,32 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
return;
}
final NumberViewState number = getModel().getNumber();
final NumberViewState number = viewModel.getNumber();
final String e164number = number.getE164Number();
if (!number.isValid()) {
Dialogs.showAlertDialog(context,
getString(R.string.RegistrationActivity_invalid_number),
String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), e164number));
getString(R.string.RegistrationActivity_invalid_number),
String.format(getString(R.string.RegistrationActivity_the_number_you_specified_s_is_invalid), e164number));
return;
}
PlayServicesUtil.PlayServicesStatus fcmStatus = PlayServicesUtil.getPlayServicesStatus(context);
if (fcmStatus == PlayServicesUtil.PlayServicesStatus.SUCCESS) {
confirmNumberPrompt(context, e164number, () -> handleRequestVerification(context, e164number, true));
confirmNumberPrompt(context, e164number, () -> handleRequestVerification(context, true));
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.MISSING) {
confirmNumberPrompt(context, e164number, () -> handlePromptForNoPlayServices(context, e164number));
confirmNumberPrompt(context, e164number, () -> handlePromptForNoPlayServices(context));
} else if (fcmStatus == PlayServicesUtil.PlayServicesStatus.NEEDS_UPDATE) {
GoogleApiAvailability.getInstance().getErrorDialog(requireActivity(), ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED, 0).show();
} else {
Dialogs.showAlertDialog(context, getString(R.string.RegistrationActivity_play_services_error),
getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable));
Dialogs.showAlertDialog(context,
getString(R.string.RegistrationActivity_play_services_error),
getString(R.string.RegistrationActivity_google_play_services_is_updating_or_unavailable));
}
}
private void handleRequestVerification(@NonNull Context context, @NonNull String e164number, boolean fcmSupported) {
private void handleRequestVerification(@NonNull Context context, boolean fcmSupported) {
setSpinning(register);
disableAllEntries();
@ -198,16 +217,16 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
task.addOnSuccessListener(none -> {
Log.i(TAG, "Successfully registered SMS listener.");
requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_WITH_LISTENER);
requestVerificationCode(Mode.SMS_WITH_LISTENER);
});
task.addOnFailureListener(e -> {
Log.w(TAG, "Failed to register SMS listener.", e);
requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER);
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
});
} else {
Log.i(TAG, "FCM is not supported, using no SMS listener");
requestVerificationCode(e164number, RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER);
requestVerificationCode(Mode.SMS_WITHOUT_LISTENER);
}
}
@ -222,77 +241,39 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
countryCode.setEnabled(true);
number.setEnabled(true);
countrySpinner.setEnabled(true);
if (isReregister()) {
if (viewModel.isReregister()) {
cancel.setVisibility(View.VISIBLE);
}
}
private void requestVerificationCode(String e164number, @NonNull RegistrationCodeRequest.Mode mode) {
RegistrationViewModel model = getModel();
String captcha = model.getCaptchaToken();
model.clearCaptchaResponse();
private void requestVerificationCode(@NonNull Mode mode) {
NavController navController = NavHostFragment.findNavController(this);
NavController navController = Navigation.findNavController(register);
Disposable request = viewModel.requestVerificationCode(mode)
.doOnSubscribe(unused -> TextSecurePreferences.setPushRegistered(ApplicationDependencies.getApplication(), false))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(processor -> {
if (processor.hasResult()) {
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
} else if (processor.localRateLimit()) {
Log.i(TAG, "Unable to request sms code due to local rate limit");
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
} else if (processor.captchaRequired()) {
Log.i(TAG, "Unable to request sms code due to captcha required");
navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
} else if (processor.rateLimit()) {
Log.i(TAG, "Unable to request sms code due to rate limit");
Toast.makeText(register.getContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show();
} else {
Log.w(TAG, "Unable to request sms code", processor.getError());
Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
}
if (!model.getRequestLimiter().canRequest(mode, e164number, System.currentTimeMillis())) {
Log.i(TAG, "Local rate limited");
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
cancelSpinning(register);
enableAllEntries();
return;
}
cancelSpinning(register);
enableAllEntries();
});
RegistrationService registrationService = RegistrationService.getInstance(e164number, model.getRegistrationSecret());
registrationService.requestVerificationCode(requireActivity(), mode, captcha,
new RegistrationCodeRequest.SmsVerificationCodeCallback() {
@Override
public void onNeedCaptcha() {
if (getContext() == null) {
Log.i(TAG, "Got onNeedCaptcha response, but fragment is no longer attached.");
return;
}
navController.navigate(EnterPhoneNumberFragmentDirections.actionRequestCaptcha());
cancelSpinning(register);
enableAllEntries();
model.getRequestLimiter().onUnsuccessfulRequest();
model.updateLimiter();
}
@Override
public void requestSent(@Nullable String fcmToken) {
if (getContext() == null) {
Log.i(TAG, "Got requestSent response, but fragment is no longer attached.");
return;
}
model.setFcmToken(fcmToken);
model.markASuccessfulAttempt();
navController.navigate(EnterPhoneNumberFragmentDirections.actionEnterVerificationCode());
cancelSpinning(register);
enableAllEntries();
model.getRequestLimiter().onSuccessfulRequest(mode, e164number, System.currentTimeMillis());
model.updateLimiter();
}
@Override
public void onRateLimited() {
Toast.makeText(register.getContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show();
cancelSpinning(register);
enableAllEntries();
model.getRequestLimiter().onUnsuccessfulRequest();
model.updateLimiter();
}
@Override
public void onError() {
Toast.makeText(register.getContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show();
cancelSpinning(register);
enableAllEntries();
model.getRequestLimiter().onUnsuccessfulRequest();
model.updateLimiter();
}
});
disposables.add(request);
}
private void initializeSpinner(Spinner countrySpinner) {
@ -368,10 +349,8 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
number.getInput().setSelection(numberLength, numberLength);
}
RegistrationViewModel model = getModel();
model.onCountrySelected(null, countryCode);
setCountryDisplay(model.getNumber().getCountryDisplayName());
viewModel.onCountrySelected(null, countryCode);
setCountryDisplay(viewModel.getNumber().getCountryDisplayName());
}
@Override
@ -391,11 +370,9 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
if (number == null) return;
RegistrationViewModel model = getModel();
viewModel.setNationalNumber(number);
model.setNationalNumber(number);
setCountryDisplay(model.getNumber().getCountryDisplayName());
setCountryDisplay(viewModel.getNumber().getCountryDisplayName());
}
@Override
@ -449,13 +426,13 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
reformatText(number.getText());
}
private void handlePromptForNoPlayServices(@NonNull Context context, @NonNull String e164number) {
new AlertDialog.Builder(context)
.setTitle(R.string.RegistrationActivity_missing_google_play_services)
.setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services)
.setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> handleRequestVerification(context, e164number, false))
.setNegativeButton(android.R.string.cancel, null)
.show();
private void handlePromptForNoPlayServices(@NonNull Context context) {
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.RegistrationActivity_missing_google_play_services)
.setMessage(R.string.RegistrationActivity_this_device_is_missing_google_play_services)
.setPositiveButton(R.string.RegistrationActivity_i_understand, (dialog1, which) -> handleRequestVerification(context, false))
.setNegativeButton(android.R.string.cancel, null)
.show();
}
protected final void confirmNumberPrompt(@NonNull Context context,
@ -466,7 +443,7 @@ public final class EnterPhoneNumberFragment extends BaseRegistrationFragment {
R.string.RegistrationActivity_a_verification_code_will_be_sent_to,
e164number,
() -> {
hideKeyboard(context, number.getInput());
ViewUtil.hideKeyboard(context, number.getInput());
onConfirmed.run();
},
() -> number.focusAndMoveCursorToEndAndOpenKeyboard());

Wyświetl plik

@ -9,16 +9,19 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.ActivityNavigator;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
public final class RegistrationCompleteFragment extends LoggingFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
@ -30,11 +33,12 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
FragmentActivity activity = requireActivity();
FragmentActivity activity = requireActivity();
RegistrationViewModel viewModel = ViewModelProviders.of(activity).get(RegistrationViewModel.class);
if (SignalStore.storageService().needsAccountRestore()) {
activity.startActivity(new Intent(activity, PinRestoreActivity.class));
} else if (!isReregister()) {
} else if (!viewModel.isReregister()) {
final Intent main = MainActivity.clearTop(activity);
final Intent profile = EditProfileActivity.getIntentForUserProfile(activity);

Wyświetl plik

@ -1,5 +1,9 @@
package org.thoughtcrime.securesms.registration.fragments;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning;
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.InputType;
@ -14,37 +18,44 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.pin.PinRestoreRepository.TokenData;
import org.thoughtcrime.securesms.registration.service.CodeVerificationRequest;
import org.thoughtcrime.securesms.registration.service.RegistrationService;
import org.thoughtcrime.securesms.pin.TokenData;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public final class RegistrationLockFragment extends BaseRegistrationFragment {
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
public final class RegistrationLockFragment extends LoggingFragment {
private static final String TAG = Log.tag(RegistrationLockFragment.class);
/** Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1. */
/**
* Applies to both V1 and V2 pins, because some V2 pins may have been migrated from V1.
*/
private static final int MINIMUM_PIN_LENGTH = 4;
private EditText pinEntry;
@ -54,6 +65,9 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
private TextView keyboardToggle;
private long timeRemaining;
private boolean isV1RegistrationLock;
private RegistrationViewModel viewModel;
private final LifecycleDisposable disposables = new LifecycleDisposable();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -87,7 +101,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
pinEntry.setImeOptions(EditorInfo.IME_ACTION_DONE);
pinEntry.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_DONE) {
hideKeyboard(requireContext(), v);
ViewUtil.hideKeyboard(requireContext(), v);
handlePinEntry();
return true;
}
@ -97,7 +111,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
enableAndFocusPinEntry();
pinButton.setOnClickListener((v) -> {
hideKeyboard(requireContext(), pinEntry);
ViewUtil.hideKeyboard(requireContext(), pinEntry);
handlePinEntry();
});
@ -111,22 +125,25 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
PinKeyboardType keyboardType = getPinEntryKeyboardType().getOther();
keyboardToggle.setText(resolveKeyboardToggleText(keyboardType));
getModel().getLockedTimeRemaining()
.observe(getViewLifecycleOwner(), t -> timeRemaining = t);
disposables.bindTo(getViewLifecycleOwner().getLifecycle());
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
TokenData keyBackupCurrentToken = getModel().getKeyBackupCurrentToken();
viewModel.getLockedTimeRemaining()
.observe(getViewLifecycleOwner(), t -> timeRemaining = t);
TokenData keyBackupCurrentToken = viewModel.getKeyBackupCurrentToken();
if (keyBackupCurrentToken != null) {
int triesRemaining = keyBackupCurrentToken.getTriesRemaining();
if (triesRemaining <= 3) {
int daysRemaining = getLockoutDays(timeRemaining);
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationLockFragment__not_many_tries_left)
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
.show();
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationLockFragment__not_many_tries_left)
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
.show();
}
if (triesRemaining < 5) {
@ -137,8 +154,8 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
private String getTriesRemainingDialogMessage(int triesRemaining, int daysRemaining) {
Resources resources = requireContext().getResources();
String tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining);
String days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining);
String tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining);
String days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining);
return tries + " " + days;
}
@ -167,114 +184,94 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
return;
}
RegistrationViewModel model = getModel();
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
TokenData tokenData = model.getKeyBackupCurrentToken();
setSpinning(pinButton);
registrationService.verifyAccount(requireActivity(),
model.getFcmToken(),
model.getTextCodeEntered(),
pin,
tokenData,
Disposable verify = viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(processor -> {
if (processor.hasResult()) {
handleSuccessfulPinEntry();
} else if (processor.wrongPin()) {
onIncorrectKbsRegistrationLockPin(processor.getToken());
} else if (processor.isKbsLocked()) {
onKbsAccountLocked();
} else if (processor.rateLimit()) {
onRateLimited();
} else {
Log.w(TAG, "Unable to verify code with registration lock", processor.getError());
onError();
}
});
new CodeVerificationRequest.VerifyCallback() {
disposables.add(verify);
}
@Override
public void onSuccessfulRegistration() {
handleSuccessfulPinEntry();
}
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenData tokenData) {
cancelSpinning(pinButton);
pinEntry.getText().clear();
enableAndFocusPinEntry();
@Override
public void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining) {
getModel().setLockedTimeRemaining(timeRemaining);
viewModel.setKeyBackupTokenData(tokenData);
cancelSpinning(pinButton);
pinEntry.getText().clear();
enableAndFocusPinEntry();
int triesRemaining = tokenData.getTriesRemaining();
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin);
}
if (triesRemaining == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.");
onAccountLocked();
return;
}
@Override
public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenData kbsTokenData, @NonNull String kbsStorageCredentials) {
throw new AssertionError("Not expected after a pin guess");
}
if (triesRemaining == 3) {
int daysRemaining = getLockoutDays(timeRemaining);
@Override
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenData tokenData) {
cancelSpinning(pinButton);
pinEntry.getText().clear();
enableAndFocusPinEntry();
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
.show();
}
model.setKeyBackupTokenData(tokenData);
if (triesRemaining > 5) {
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again);
} else {
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining));
forgotPin.setVisibility(View.VISIBLE);
}
}
int triesRemaining = tokenData.getTriesRemaining();
if (triesRemaining == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.");
onAccountLocked();
return;
}
public void onRateLimited() {
cancelSpinning(pinButton);
enableAndFocusPinEntry();
if (triesRemaining == 3) {
int daysRemaining = getLockoutDays(timeRemaining);
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationActivity_too_many_attempts)
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
.setPositiveButton(android.R.string.ok, null)
.show();
}
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationLockFragment__incorrect_pin)
.setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
.setPositiveButton(android.R.string.ok, null)
.show();
}
if (triesRemaining > 5) {
errorLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again);
} else {
errorLabel.setText(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, triesRemaining, triesRemaining));
forgotPin.setVisibility(View.VISIBLE);
}
}
public void onKbsAccountLocked() {
onAccountLocked();
}
@Override
public void onRateLimited() {
cancelSpinning(pinButton);
enableAndFocusPinEntry();
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationActivity_too_many_attempts)
.setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
.setPositiveButton(android.R.string.ok, null)
.show();
}
public void onError() {
cancelSpinning(pinButton);
enableAndFocusPinEntry();
@Override
public void onKbsAccountLocked(@Nullable Long timeRemaining) {
if (timeRemaining != null) {
model.setLockedTimeRemaining(timeRemaining);
}
onAccountLocked();
}
@Override
public void onError() {
cancelSpinning(pinButton);
enableAndFocusPinEntry();
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
}
});
Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show();
}
private void handleForgottenPin(long timeRemainingMs) {
int lockoutDays = getLockoutDays(timeRemainingMs);
new AlertDialog.Builder(requireContext())
.setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
.setMessage(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
.show();
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
.setMessage(requireContext().getResources().getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.PinRestoreEntryFragment_contact_support, (dialog, which) -> sendEmailToSupport())
.show();
}
private static int getLockoutDays(long timeRemainingMs) {
@ -288,7 +285,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
private void updateKeyboard(@NonNull PinKeyboardType keyboard) {
boolean isAlphaNumeric = keyboard == PinKeyboardType.ALPHA_NUMERIC;
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
pinEntry.setInputType(isAlphaNumeric ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD
: InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
pinEntry.getText().clear();

Wyświetl plik

@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.registration.fragments
import android.content.Context
import android.content.Intent
import android.text.SpannableStringBuilder
import android.view.View
import android.widget.Toast
import androidx.annotation.StringRes
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.TranslationDetection
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.util.SpanUtil
object RegistrationViewDelegate {
@JvmStatic
fun setDebugLogSubmitMultiTapView(view: View?) {
view?.setOnClickListener(object : View.OnClickListener {
private val DEBUG_TAP_TARGET = 8
private val DEBUG_TAP_ANNOUNCE = 4
private var debugTapCounter = 0
override fun onClick(view: View) {
debugTapCounter++
if (debugTapCounter >= DEBUG_TAP_TARGET) {
view.context.startActivity(Intent(view.context, SubmitDebugLogActivity::class.java))
} else if (debugTapCounter >= DEBUG_TAP_ANNOUNCE) {
val remaining = DEBUG_TAP_TARGET - debugTapCounter
Toast.makeText(view.context, view.context.resources.getQuantityString(R.plurals.RegistrationActivity_debug_log_hint, remaining, remaining), Toast.LENGTH_SHORT).show()
}
}
})
}
@JvmStatic
fun showConfirmNumberDialogIfTranslated(
context: Context,
@StringRes firstMessageLine: Int,
e164number: String,
onConfirmed: Runnable,
onEditNumber: Runnable
) {
val translationDetection = TranslationDetection(context)
if (translationDetection.textExistsInUsersLanguage(
firstMessageLine,
R.string.RegistrationActivity_is_your_phone_number_above_correct,
R.string.RegistrationActivity_edit_number
)
) {
val message: CharSequence = SpannableStringBuilder()
.append(context.getString(firstMessageLine))
.append("\n\n")
.append(SpanUtil.bold(PhoneNumberFormatter.prettyPrint(e164number)))
.append("\n\n")
.append(context.getString(R.string.RegistrationActivity_is_your_phone_number_above_correct))
MaterialAlertDialogBuilder(context)
.setMessage(message)
.setPositiveButton(android.R.string.ok) { _, _ -> onConfirmed.run() }
.setNegativeButton(R.string.RegistrationActivity_edit_number) { _, _ -> onEditNumber.run() }
.show()
} else {
onConfirmed.run()
}
}
}

Wyświetl plik

@ -1,5 +1,9 @@
package org.thoughtcrime.securesms.registration.fragments;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning;
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
@ -27,6 +31,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.Navigation;
import com.dd.CircularProgressButton;
@ -38,6 +43,7 @@ import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.AppInitialization;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
import org.thoughtcrime.securesms.backup.FullBackupBase;
@ -47,6 +53,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.NoExternalStorageException;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.DateUtils;
@ -57,7 +64,7 @@ import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.io.IOException;
import java.util.Locale;
public final class RestoreBackupFragment extends BaseRegistrationFragment {
public final class RestoreBackupFragment extends LoggingFragment {
private static final String TAG = Log.tag(RestoreBackupFragment.class);
private static final short OPEN_DOCUMENT_TREE_RESULT_CODE = 13782;
@ -94,7 +101,8 @@ public final class RestoreBackupFragment extends BaseRegistrationFragment {
.navigate(RestoreBackupFragmentDirections.actionSkip());
});
if (isReregister()) {
RegistrationViewModel viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
if (viewModel.isReregister()) {
Log.i(TAG, "Skipping backup restore during re-register.");
Navigation.findNavController(view)
.navigate(RestoreBackupFragmentDirections.actionSkipNoReturn());

Wyświetl plik

@ -1,5 +1,9 @@
package org.thoughtcrime.securesms.registration.fragments;
import static org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate.setDebugLogSubmitMultiTapView;
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.cancelSpinning;
import static org.thoughtcrime.securesms.util.CircularProgressButtonUtil.setSpinning;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
@ -16,8 +20,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProviders;
import androidx.navigation.ActivityNavigator;
import androidx.navigation.Navigation;
import androidx.navigation.fragment.NavHostFragment;
@ -30,6 +34,7 @@ import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.logging.Log;
import org.signal.devicetransfer.DeviceToDeviceTransferService;
import org.signal.devicetransfer.TransferStatus;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
@ -40,52 +45,49 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
public final class WelcomeFragment extends BaseRegistrationFragment {
public final class WelcomeFragment extends LoggingFragment {
private static final String TAG = Log.tag(WelcomeFragment.class);
private static final String[] PERMISSIONS = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE };
private static final String[] PERMISSIONS = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE };
@RequiresApi(26)
private static final String[] PERMISSIONS_API_26 = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_PHONE_NUMBERS };
private static final String[] PERMISSIONS_API_26 = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_PHONE_NUMBERS };
@RequiresApi(26)
private static final String[] PERMISSIONS_API_29 = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_PHONE_NUMBERS };
private static final @StringRes int RATIONALE = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends;
private static final @StringRes int RATIONALE_API_29 = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends;
private static final int[] HEADERS = { R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp };
private static final int[] HEADERS_API_29 = { R.drawable.ic_contacts_white_48dp };
private static final String[] PERMISSIONS_API_29 = { Manifest.permission.WRITE_CONTACTS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.READ_PHONE_STATE,
Manifest.permission.READ_PHONE_NUMBERS };
private static final @StringRes int RATIONALE = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_and_media_in_order_to_connect_with_friends;
private static final @StringRes int RATIONALE_API_29 = R.string.RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends;
private static final int[] HEADERS = { R.drawable.ic_contacts_white_48dp, R.drawable.ic_folder_white_48dp };
private static final int[] HEADERS_API_29 = { R.drawable.ic_contacts_white_48dp };
private CircularProgressButton continueButton;
private Button restoreFromBackup;
private RegistrationViewModel viewModel;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(isReregister() ? R.layout.fragment_registration_blank
: R.layout.fragment_registration_welcome,
container,
false);
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_registration_welcome, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (isReregister()) {
RegistrationViewModel model = getModel();
viewModel = ViewModelProviders.of(requireActivity()).get(RegistrationViewModel.class);
if (model.hasRestoreFlowBeenShown()) {
if (viewModel.isReregister()) {
if (viewModel.hasRestoreFlowBeenShown()) {
Log.i(TAG, "We've come back to the home fragment on a restore, user must be backing out");
if (!Navigation.findNavController(view).popBackStack()) {
FragmentActivity activity = requireActivity();
@ -98,10 +100,9 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
initializeNumber();
Log.i(TAG, "Skipping restore because this is a reregistration.");
model.setWelcomeSkippedOnRestore();
viewModel.setWelcomeSkippedOnRestore();
Navigation.findNavController(view)
.navigate(WelcomeFragmentDirections.actionSkipRestore());
} else {
setDebugLogSubmitMultiTapView(view.findViewById(R.id.image));
@ -110,7 +111,7 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
continueButton = view.findViewById(R.id.welcome_continue_button);
continueButton.setOnClickListener(this::continueClicked);
restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore);
Button restoreFromBackup = view.findViewById(R.id.welcome_transfer_or_restore);
restoreFromBackup.setOnClickListener(this::restoreFromBackupClicked);
TextView welcomeTermsButton = view.findViewById(R.id.welcome_terms_button);
@ -211,13 +212,13 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
Phonenumber.PhoneNumber phoneNumber = localNumber.get();
String nationalNumber = PhoneNumberUtil.getInstance().format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.NATIONAL);
getModel().onNumberDetected(phoneNumber.getCountryCode(), nationalNumber);
viewModel.onNumberDetected(phoneNumber.getCountryCode(), nationalNumber);
} else {
Log.i(TAG, "No number detected");
Optional<String> simCountryIso = Util.getSimCountryIso(requireContext());
if (simCountryIso.isPresent() && !TextUtils.isEmpty(simCountryIso.get())) {
getModel().onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), "");
viewModel.onNumberDetected(PhoneNumberUtil.getInstance().getCountryCodeForRegion(simCountryIso.get()), "");
}
}
}
@ -228,7 +229,7 @@ public final class WelcomeFragment extends BaseRegistrationFragment {
private boolean canUserSelectBackup() {
return BackupUtil.isUserSelectionRequired(requireContext()) &&
!isReregister() &&
!viewModel.isReregister() &&
!SignalStore.settings().isBackupEnabled();
}

Wyświetl plik

@ -1,316 +0,0 @@
package org.thoughtcrime.securesms.registration.service;
import android.content.Context;
import android.os.AsyncTask;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.AppCapabilities;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.PreKeyUtil;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.crypto.SenderKeyUtil;
import org.thoughtcrime.securesms.crypto.SessionUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.pin.PinRestoreRepository;
import org.thoughtcrime.securesms.pin.PinRestoreRepository.TokenData;
import org.thoughtcrime.securesms.pin.PinState;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.KeyHelper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.KbsPinData;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.LockedException;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
public final class CodeVerificationRequest {
private static final String TAG = Log.tag(CodeVerificationRequest.class);
private enum Result {
SUCCESS,
PIN_LOCKED,
KBS_WRONG_PIN,
RATE_LIMITED,
KBS_ACCOUNT_LOCKED,
ERROR
}
/**
* Asynchronously verify the account via the code.
*
* @param fcmToken The FCM token for the device.
* @param code The code that was delivered to the user.
* @param pin The users registration pin.
* @param callback Exactly one method on this callback will be called.
* @param kbsTokenData By keeping the token, on failure, a newly returned token will be reused in subsequent pin
* attempts, preventing certain attacks, we can also track the attempts making missing replies easier to spot.
*/
static void verifyAccount(@NonNull Context context,
@NonNull Credentials credentials,
@Nullable String fcmToken,
@NonNull String code,
@Nullable String pin,
@Nullable TokenData kbsTokenData,
@NonNull VerifyCallback callback)
{
new AsyncTask<Void, Void, Result>() {
private volatile LockedException lockedException;
private volatile TokenData tokenData;
@Override
protected Result doInBackground(Void... voids) {
final boolean pinSupplied = pin != null;
final boolean tryKbs = tokenData != null;
try {
this.tokenData = kbsTokenData;
verifyAccount(context, credentials, code, pin, tokenData, fcmToken);
return Result.SUCCESS;
} catch (KeyBackupSystemNoDataException e) {
Log.w(TAG, "No data found on KBS");
return Result.KBS_ACCOUNT_LOCKED;
} catch (KeyBackupSystemWrongPinException e) {
tokenData = TokenData.withResponse(tokenData, e.getTokenResponse());
return Result.KBS_WRONG_PIN;
} catch (LockedException e) {
if (pinSupplied && tryKbs) {
throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
}
Log.w(TAG, e);
lockedException = e;
if (e.getBasicStorageCredentials() != null) {
try {
tokenData = getToken(e.getBasicStorageCredentials());
if (tokenData == null || tokenData.getTriesRemaining() == 0) {
return Result.KBS_ACCOUNT_LOCKED;
}
} catch (IOException ex) {
Log.w(TAG, e);
return Result.ERROR;
}
}
return Result.PIN_LOCKED;
} catch (RateLimitException e) {
Log.w(TAG, e);
return Result.RATE_LIMITED;
} catch (IOException e) {
Log.w(TAG, e);
return Result.ERROR;
}
}
@Override
protected void onPostExecute(Result result) {
switch (result) {
case SUCCESS:
handleSuccessfulRegistration(context);
callback.onSuccessfulRegistration();
break;
case PIN_LOCKED:
if (tokenData != null) {
if (lockedException.getBasicStorageCredentials() == null) {
throw new AssertionError("KBS Token set, but no storage credentials supplied.");
}
Log.w(TAG, "Reg Locked: V2 pin needed for registration");
callback.onKbsRegistrationLockPinRequired(lockedException.getTimeRemaining(), tokenData, lockedException.getBasicStorageCredentials());
} else {
Log.w(TAG, "Reg Locked: V1 pin needed for registration");
callback.onV1RegistrationLockPinRequiredOrIncorrect(lockedException.getTimeRemaining());
}
break;
case RATE_LIMITED:
callback.onRateLimited();
break;
case ERROR:
callback.onError();
break;
case KBS_WRONG_PIN:
Log.w(TAG, "KBS Pin was wrong");
callback.onIncorrectKbsRegistrationLockPin(tokenData);
break;
case KBS_ACCOUNT_LOCKED:
Log.w(TAG, "KBS Account is locked");
callback.onKbsAccountLocked(lockedException != null ? lockedException.getTimeRemaining() : null);
break;
}
}
}.executeOnExecutor(SignalExecutors.UNBOUNDED);
}
private static TokenData getToken(@Nullable String basicStorageCredentials) throws IOException {
if (basicStorageCredentials == null) return null;
return new PinRestoreRepository().getTokenSync(basicStorageCredentials);
}
private static void handleSuccessfulRegistration(@NonNull Context context) {
JobManager jobManager = ApplicationDependencies.getJobManager();
jobManager.add(new DirectoryRefreshJob(false));
jobManager.add(new RotateCertificateJob());
DirectoryRefreshListener.schedule(context);
RotateSignedPreKeyListener.schedule(context);
}
private static void verifyAccount(@NonNull Context context,
@NonNull Credentials credentials,
@NonNull String code,
@Nullable String pin,
@Nullable TokenData kbsTokenData,
@Nullable String fcmToken)
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
{
boolean isV2RegistrationLock = kbsTokenData != null;
int registrationId = KeyHelper.generateRegistrationId(false);
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
ProfileKey profileKey = findExistingProfileKey(context, credentials.getE164number());
if (profileKey == null) {
profileKey = ProfileKeyUtil.createNew();
Log.i(TAG, "No profile key found, created a new one");
}
byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(profileKey);
TextSecurePreferences.setLocalRegistrationId(context, registrationId);
SessionUtil.archiveAllSessions();
SenderKeyUtil.clearAllState(context);
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
KbsPinData kbsData = isV2RegistrationLock ? PinState.restoreMasterKey(pin, kbsTokenData.getEnclave(), kbsTokenData.getBasicAuth(), kbsTokenData.getTokenResponse()) : null;
String registrationLockV2 = kbsData != null ? kbsData.getMasterKey().deriveRegistrationLock() : null;
String registrationLockV1 = isV2RegistrationLock ? null : pin;
boolean hasFcm = fcmToken != null;
Log.i(TAG, "Calling verifyAccountWithCode(): reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2));
VerifyAccountResponse response = accountManager.verifyAccountWithCode(code,
null,
registrationId,
!hasFcm,
registrationLockV1,
registrationLockV2,
unidentifiedAccessKey,
universalUnidentifiedAccess,
AppCapabilities.getCapabilities(true),
SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isDiscoverable());
UUID uuid = UuidUtil.parseOrThrow(response.getUuid());
boolean hasPin = response.isStorageCapable();
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context);
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(context);
SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);
accountManager = AccountManagerFactory.createAuthenticated(context, uuid, credentials.getE164number(), credentials.getPassword());
accountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);
if (hasFcm) {
accountManager.setGcmId(Optional.fromNullable(fcmToken));
}
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
RecipientId selfId = Recipient.externalPush(context, uuid, credentials.getE164number(), true).getId();
recipientDatabase.setProfileSharing(selfId, true);
recipientDatabase.markRegisteredOrThrow(selfId, uuid);
TextSecurePreferences.setLocalNumber(context, credentials.getE164number());
TextSecurePreferences.setLocalUuid(context, uuid);
recipientDatabase.setProfileKey(selfId, profileKey);
ApplicationDependencies.getRecipientCache().clearSelf();
TextSecurePreferences.setFcmToken(context, fcmToken);
TextSecurePreferences.setFcmDisabled(context, !hasFcm);
TextSecurePreferences.setWebsocketRegistered(context, true);
DatabaseFactory.getIdentityDatabase(context)
.saveIdentity(selfId,
identityKey.getPublicKey(), IdentityDatabase.VerifiedStatus.VERIFIED,
true, System.currentTimeMillis(), true);
TextSecurePreferences.setPushRegistered(context, true);
TextSecurePreferences.setPushServerPassword(context, credentials.getPassword());
TextSecurePreferences.setSignedPreKeyRegistered(context, true);
TextSecurePreferences.setPromptedPushRegistration(context, true);
TextSecurePreferences.setUnauthorizedReceived(context, false);
PinState.onRegistration(context, kbsData, pin, hasPin);
}
private static @Nullable ProfileKey findExistingProfileKey(@NonNull Context context, @NonNull String e164number) {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
Optional<RecipientId> recipient = recipientDatabase.getByE164(e164number);
if (recipient.isPresent()) {
return ProfileKeyUtil.profileKeyOrNull(Recipient.resolved(recipient.get()).getProfileKey());
}
return null;
}
public interface VerifyCallback {
void onSuccessfulRegistration();
/**
* The account is locked with a V1 (non-KBS) pin.
*
* @param timeRemaining Time until pin expires and number can be reused.
*/
void onV1RegistrationLockPinRequiredOrIncorrect(long timeRemaining);
/**
* The account is locked with a V2 (KBS) pin. Called before any user pin guesses.
*/
void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenData kbsTokenData, @NonNull String kbsStorageCredentials);
/**
* The account is locked with a V2 (KBS) pin. Called after a user pin guess.
* <p>
* i.e. an attempt has likely been used.
*/
void onIncorrectKbsRegistrationLockPin(@NonNull TokenData kbsTokenResponse);
/**
* V2 (KBS) pin is set, but there is no data on KBS.
*
* @param timeRemaining Non-null if known.
*/
void onKbsAccountLocked(@Nullable Long timeRemaining);
void onRateLimited();
void onError();
}
}

Wyświetl plik

@ -1,22 +0,0 @@
package org.thoughtcrime.securesms.registration.service;
import androidx.annotation.NonNull;
public final class Credentials {
private final String e164number;
private final String password;
public Credentials(@NonNull String e164number, @NonNull String password) {
this.e164number = e164number;
this.password = password;
}
public @NonNull String getE164number() {
return e164number;
}
public @NonNull String getPassword() {
return password;
}
}

Wyświetl plik

@ -1,146 +0,0 @@
package org.thoughtcrime.securesms.registration.service;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.gcm.FcmUtil;
import org.thoughtcrime.securesms.push.AccountManagerFactory;
import org.thoughtcrime.securesms.registration.PushChallengeRequest;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import java.io.IOException;
import java.util.Locale;
public final class RegistrationCodeRequest {
private static final long PUSH_REQUEST_TIMEOUT_MS = 5000L;
private static final String TAG = Log.tag(RegistrationCodeRequest.class);
/**
* Request a verification code to be sent according to the specified {@param mode}.
*
* The request will fire asynchronously, and exactly one of the methods on the {@param callback}
* will be called.
*/
@SuppressLint("StaticFieldLeak")
static void requestSmsVerificationCode(@NonNull Context context, @NonNull Credentials credentials, @Nullable String captchaToken, @NonNull Mode mode, @NonNull SmsVerificationCodeCallback callback) {
Log.d(TAG, "SMS Verification requested");
new AsyncTask<Void, Void, VerificationRequestResult>() {
@Override
protected @NonNull
VerificationRequestResult doInBackground(Void... voids) {
try {
markAsVerifying(context);
Optional<String> fcmToken = FcmUtil.getToken();
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
Optional<String> pushChallenge = PushChallengeRequest.getPushChallengeBlocking(accountManager, fcmToken, credentials.getE164number(), PUSH_REQUEST_TIMEOUT_MS);
if (mode == Mode.PHONE_CALL) {
accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.fromNullable(captchaToken), pushChallenge);
} else {
accountManager.requestSmsVerificationCode(mode.isSmsRetrieverSupported(), Optional.fromNullable(captchaToken), pushChallenge);
}
return new VerificationRequestResult(fcmToken.orNull(), Optional.absent());
} catch (IOException e) {
org.signal.core.util.logging.Log.w(TAG, "Error during account registration", e);
return new VerificationRequestResult(null, Optional.of(e));
}
}
protected void onPostExecute(@NonNull VerificationRequestResult result) {
if (isCaptchaRequired(result)) {
callback.onNeedCaptcha();
} else if (isRateLimited(result)) {
callback.onRateLimited();
} else if (result.exception.isPresent()) {
callback.onError();
} else {
callback.requestSent(result.fcmToken);
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private static void markAsVerifying(Context context) {
TextSecurePreferences.setPushRegistered(context, false);
}
private static boolean isCaptchaRequired(@NonNull VerificationRequestResult result) {
return result.exception.isPresent() && result.exception.get() instanceof CaptchaRequiredException;
}
private static boolean isRateLimited(@NonNull VerificationRequestResult result) {
return result.exception.isPresent() && result.exception.get() instanceof RateLimitException;
}
private static class VerificationRequestResult {
private final @Nullable String fcmToken;
private final Optional<IOException> exception;
private VerificationRequestResult(@Nullable String fcmToken, Optional<IOException> exception) {
this.fcmToken = fcmToken;
this.exception = exception;
}
}
/**
* The mode by which a code is being requested.
*/
public enum Mode {
/**
* Device is requesting an SMS and supports SMS retrieval.
*
* The SMS sent will be formatted for automatic SMS retrieval.
*/
SMS_WITH_LISTENER(true),
/**
* Device is requesting an SMS and does not support SMS retrieval.
*
* The SMS sent will be not be specially formatted for automatic SMS retrieval.
*/
SMS_WITHOUT_LISTENER(false),
/**
* Device is requesting a phone call.
*/
PHONE_CALL(false);
private final boolean smsRetrieverSupported;
Mode(boolean smsRetrieverSupported) {
this.smsRetrieverSupported = smsRetrieverSupported;
}
public boolean isSmsRetrieverSupported() {
return smsRetrieverSupported;
}
}
public interface SmsVerificationCodeCallback {
void onNeedCaptcha();
void requestSent(@Nullable String fcmToken);
void onRateLimited();
void onError();
}
}

Wyświetl plik

@ -1,45 +0,0 @@
package org.thoughtcrime.securesms.registration.service;
import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.pin.PinRestoreRepository;
public final class RegistrationService {
private final Credentials credentials;
private RegistrationService(@NonNull Credentials credentials) {
this.credentials = credentials;
}
public static RegistrationService getInstance(@NonNull String e164number, @NonNull String password) {
return new RegistrationService(new Credentials(e164number, password));
}
/**
* See {@link RegistrationCodeRequest}.
*/
public void requestVerificationCode(@NonNull Activity activity,
@NonNull RegistrationCodeRequest.Mode mode,
@Nullable String captchaToken,
@NonNull RegistrationCodeRequest.SmsVerificationCodeCallback callback)
{
RegistrationCodeRequest.requestSmsVerificationCode(activity, credentials, captchaToken, mode, callback);
}
/**
* See {@link CodeVerificationRequest}.
*/
public void verifyAccount(@NonNull Activity activity,
@Nullable String fcmToken,
@NonNull String code,
@Nullable String pin,
@Nullable PinRestoreRepository.TokenData tokenData,
@NonNull CodeVerificationRequest.VerifyCallback callback)
{
CodeVerificationRequest.verifyAccount(activity, credentials, fcmToken, code, pin, tokenData, callback);
}
}

Wyświetl plik

@ -6,7 +6,7 @@ import android.os.Parcelable;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
import java.util.HashMap;
import java.util.Map;
@ -14,8 +14,8 @@ import java.util.Objects;
public final class LocalCodeRequestRateLimiter implements Parcelable {
private final long timePeriod;
private final Map<RegistrationCodeRequest.Mode, Data> dataMap;
private final long timePeriod;
private final Map<Mode, Data> dataMap;
public LocalCodeRequestRateLimiter(long timePeriod) {
this.timePeriod = timePeriod;
@ -23,7 +23,7 @@ public final class LocalCodeRequestRateLimiter implements Parcelable {
}
@MainThread
public boolean canRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) {
public boolean canRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) {
Data data = dataMap.get(mode);
return data == null || !data.limited(e164Number, currentTime);
@ -33,7 +33,7 @@ public final class LocalCodeRequestRateLimiter implements Parcelable {
* Call this when the server has returned that it was successful in requesting a code via the specified mode.
*/
@MainThread
public void onSuccessfulRequest(@NonNull RegistrationCodeRequest.Mode mode, @NonNull String e164Number, long currentTime) {
public void onSuccessfulRequest(@NonNull Mode mode, @NonNull String e164Number, long currentTime) {
dataMap.put(mode, new Data(e164Number, currentTime + timePeriod));
}
@ -69,9 +69,9 @@ public final class LocalCodeRequestRateLimiter implements Parcelable {
LocalCodeRequestRateLimiter localCodeRequestRateLimiter = new LocalCodeRequestRateLimiter(timePeriod);
for (int i = 0; i < numberOfMapEntries; i++) {
RegistrationCodeRequest.Mode mode = RegistrationCodeRequest.Mode.values()[in.readInt()];
String e164Number = in.readString();
long limitedUntil = in.readLong();
Mode mode = Mode.values()[in.readInt()];
String e164Number = in.readString();
long limitedUntil = in.readLong();
localCodeRequestRateLimiter.dataMap.put(mode, new Data(Objects.requireNonNull(e164Number), limitedUntil));
}
@ -94,7 +94,7 @@ public final class LocalCodeRequestRateLimiter implements Parcelable {
dest.writeLong(timePeriod);
dest.writeInt(dataMap.size());
for (Map.Entry<RegistrationCodeRequest.Mode, Data> a : dataMap.entrySet()) {
for (Map.Entry<Mode, Data> a : dataMap.entrySet()) {
dest.writeInt(a.getKey().ordinal());
dest.writeString(a.getValue().e164Number);
dest.writeLong(a.getValue().limitedUntil);

Wyświetl plik

@ -3,90 +3,117 @@ package org.thoughtcrime.securesms.registration.viewmodel;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.AbstractSavedStateViewModelFactory;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;
import androidx.savedstate.SavedStateRegistryOwner;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.pin.PinRestoreRepository.TokenData;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.pin.KbsRepository;
import org.thoughtcrime.securesms.pin.TokenData;
import org.thoughtcrime.securesms.registration.RegistrationData;
import org.thoughtcrime.securesms.registration.RegistrationRepository;
import org.thoughtcrime.securesms.registration.RequestVerificationCodeResponseProcessor;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository;
import org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor;
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithFailedKbs;
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithSuccessfulKbs;
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs;
import org.thoughtcrime.securesms.registration.VerifyCodeWithRegistrationLockResponseProcessor;
import org.thoughtcrime.securesms.util.Util;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public final class RegistrationViewModel extends ViewModel {
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Single;
private static final String TAG = Log.tag(RegistrationViewModel.class);
public final class RegistrationViewModel extends ViewModel {
private static final long FIRST_CALL_AVAILABLE_AFTER_MS = TimeUnit.SECONDS.toMillis(64);
private static final long SUBSEQUENT_CALL_AVAILABLE_AFTER_MS = TimeUnit.SECONDS.toMillis(300);
private final String secret;
private final MutableLiveData<NumberViewState> number;
private final MutableLiveData<String> textCodeEntered;
private final MutableLiveData<String> captchaToken;
private final MutableLiveData<String> fcmToken;
private final MutableLiveData<Boolean> restoreFlowShown;
private final MutableLiveData<Integer> successfulCodeRequestAttempts;
private final MutableLiveData<LocalCodeRequestRateLimiter> requestLimiter;
private final MutableLiveData<TokenData> kbsTokenData;
private final MutableLiveData<Long> lockedTimeRemaining;
private final MutableLiveData<Long> canCallAtTime;
private static final String STATE_REGISTRATION_SECRET = "REGISTRATION_SECRET";
private static final String STATE_NUMBER = "NUMBER";
private static final String STATE_VERIFICATION_CODE = "TEXT_CODE_ENTERED";
private static final String STATE_CAPTCHA = "CAPTCHA";
private static final String STATE_FCM_TOKEN = "FCM_TOKEN";
private static final String STATE_RESTORE_FLOW_SHOWN = "RESTORE_FLOW_SHOWN";
private static final String STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS = "SUCCESSFUL_CODE_REQUEST_ATTEMPTS";
private static final String STATE_REQUEST_RATE_LIMITER = "REQUEST_RATE_LIMITER";
private static final String STATE_KBS_TOKEN = "KBS_TOKEN";
private static final String STATE_TIME_REMAINING = "TIME_REMAINING";
private static final String STATE_CAN_CALL_AT_TIME = "CAN_CALL_AT_TIME";
private static final String STATE_IS_REREGISTER = "IS_REREGISTER";
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle) {
secret = loadValue(savedStateHandle, "REGISTRATION_SECRET", Util.getSecret(18));
private final SavedStateHandle registrationState;
private final VerifyAccountRepository verifyAccountRepository;
private final KbsRepository kbsRepository;
private final RegistrationRepository registrationRepository;
number = savedStateHandle.getLiveData("NUMBER", NumberViewState.INITIAL);
textCodeEntered = savedStateHandle.getLiveData("TEXT_CODE_ENTERED", "");
captchaToken = savedStateHandle.getLiveData("CAPTCHA");
fcmToken = savedStateHandle.getLiveData("FCM_TOKEN");
restoreFlowShown = savedStateHandle.getLiveData("RESTORE_FLOW_SHOWN", false);
successfulCodeRequestAttempts = savedStateHandle.getLiveData("SUCCESSFUL_CODE_REQUEST_ATTEMPTS", 0);
requestLimiter = savedStateHandle.getLiveData("REQUEST_RATE_LIMITER", new LocalCodeRequestRateLimiter(60_000));
kbsTokenData = savedStateHandle.getLiveData("KBS_TOKEN");
lockedTimeRemaining = savedStateHandle.getLiveData("TIME_REMAINING", 0L);
canCallAtTime = savedStateHandle.getLiveData("CAN_CALL_AT_TIME", 0L);
public RegistrationViewModel(@NonNull SavedStateHandle savedStateHandle,
boolean isReregister,
@NonNull VerifyAccountRepository verifyAccountRepository,
@NonNull KbsRepository kbsRepository,
@NonNull RegistrationRepository registrationRepository)
{
this.registrationState = savedStateHandle;
this.verifyAccountRepository = verifyAccountRepository;
this.kbsRepository = kbsRepository;
this.registrationRepository = registrationRepository;
setInitialDefaultValue(this.registrationState, STATE_REGISTRATION_SECRET, Util.getSecret(18));
setInitialDefaultValue(this.registrationState, STATE_NUMBER, NumberViewState.INITIAL);
setInitialDefaultValue(this.registrationState, STATE_VERIFICATION_CODE, "");
setInitialDefaultValue(this.registrationState, STATE_RESTORE_FLOW_SHOWN, false);
setInitialDefaultValue(this.registrationState, STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0);
setInitialDefaultValue(this.registrationState, STATE_REQUEST_RATE_LIMITER, new LocalCodeRequestRateLimiter(60_000));
this.registrationState.set(STATE_IS_REREGISTER, isReregister);
}
private static <T> T loadValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
if (!savedStateHandle.contains(key)) {
private static <T> void setInitialDefaultValue(@NonNull SavedStateHandle savedStateHandle, @NonNull String key, @NonNull T initialValue) {
if (!savedStateHandle.contains(key) || savedStateHandle.get(key) == null) {
savedStateHandle.set(key, initialValue);
}
return savedStateHandle.get(key);
}
public boolean isReregister() {
//noinspection ConstantConditions
return registrationState.get(STATE_IS_REREGISTER);
}
public @NonNull NumberViewState getNumber() {
//noinspection ConstantConditions Live data was given an initial value
return number.getValue();
}
public @NonNull LiveData<NumberViewState> getLiveNumber() {
return number;
//noinspection ConstantConditions
return registrationState.get(STATE_NUMBER);
}
public @NonNull String getTextCodeEntered() {
//noinspection ConstantConditions Live data was given an initial value
return textCodeEntered.getValue();
//noinspection ConstantConditions
return registrationState.get(STATE_VERIFICATION_CODE);
}
public String getCaptchaToken() {
return captchaToken.getValue();
private @Nullable String getCaptchaToken() {
return registrationState.get(STATE_CAPTCHA);
}
public boolean hasCaptchaToken() {
return getCaptchaToken() != null;
}
public String getRegistrationSecret() {
return secret;
private @NonNull String getRegistrationSecret() {
//noinspection ConstantConditions
return registrationState.get(STATE_REGISTRATION_SECRET);
}
public void onCaptchaResponse(String captchaToken) {
this.captchaToken.setValue(captchaToken);
public void setCaptchaResponse(@Nullable String captchaToken) {
registrationState.set(STATE_CAPTCHA, captchaToken);
}
public void clearCaptchaResponse() {
captchaToken.setValue(null);
private void clearCaptchaResponse() {
setCaptchaResponse(null);
}
public void onCountrySelected(@Nullable String selectedCountryName, int countryCode) {
@ -102,13 +129,13 @@ public final class RegistrationViewModel extends ViewModel {
private void setViewState(NumberViewState numberViewState) {
if (!numberViewState.equals(getNumber())) {
number.setValue(numberViewState);
registrationState.set(STATE_NUMBER, numberViewState);
}
}
@MainThread
public void onVerificationCodeEntered(String code) {
textCodeEntered.setValue(code);
registrationState.set(STATE_VERIFICATION_CODE, code);
}
public void onNumberDetected(int countryCode, String nationalNumber) {
@ -118,67 +145,183 @@ public final class RegistrationViewModel extends ViewModel {
.build());
}
public String getFcmToken() {
return fcmToken.getValue();
private @Nullable String getFcmToken() {
return registrationState.get(STATE_FCM_TOKEN);
}
@MainThread
public void setFcmToken(@Nullable String fcmToken) {
this.fcmToken.setValue(fcmToken);
registrationState.set(STATE_FCM_TOKEN, fcmToken);
}
public void setWelcomeSkippedOnRestore() {
restoreFlowShown.setValue(true);
registrationState.set(STATE_RESTORE_FLOW_SHOWN, true);
}
public boolean hasRestoreFlowBeenShown() {
//noinspection ConstantConditions Live data was given an initial value
return restoreFlowShown.getValue();
//noinspection ConstantConditions
return registrationState.get(STATE_RESTORE_FLOW_SHOWN);
}
public void markASuccessfulAttempt() {
//noinspection ConstantConditions Live data was given an initial value
successfulCodeRequestAttempts.setValue(successfulCodeRequestAttempts.getValue() + 1);
private void markASuccessfulAttempt() {
//noinspection ConstantConditions
registrationState.set(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, (Integer) registrationState.get(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS) + 1);
}
public LiveData<Integer> getSuccessfulCodeRequestAttempts() {
return successfulCodeRequestAttempts;
return registrationState.getLiveData(STATE_SUCCESSFUL_CODE_REQUEST_ATTEMPTS, 0);
}
public @NonNull LocalCodeRequestRateLimiter getRequestLimiter() {
//noinspection ConstantConditions Live data was given an initial value
return requestLimiter.getValue();
private @NonNull LocalCodeRequestRateLimiter getRequestLimiter() {
//noinspection ConstantConditions
return registrationState.get(STATE_REQUEST_RATE_LIMITER);
}
public void updateLimiter() {
requestLimiter.setValue(requestLimiter.getValue());
private void updateLimiter() {
registrationState.set(STATE_REQUEST_RATE_LIMITER, registrationState.get(STATE_REQUEST_RATE_LIMITER));
}
public @Nullable TokenData getKeyBackupCurrentToken() {
return kbsTokenData.getValue();
return registrationState.get(STATE_KBS_TOKEN);
}
public void setKeyBackupTokenData(TokenData tokenData) {
kbsTokenData.setValue(tokenData);
public void setKeyBackupTokenData(@Nullable TokenData tokenData) {
registrationState.set(STATE_KBS_TOKEN, tokenData);
}
public LiveData<Long> getLockedTimeRemaining() {
return lockedTimeRemaining;
return registrationState.getLiveData(STATE_TIME_REMAINING, 0L);
}
public LiveData<Long> getCanCallAtTime() {
return canCallAtTime;
return registrationState.getLiveData(STATE_CAN_CALL_AT_TIME, 0L);
}
public void setLockedTimeRemaining(long lockedTimeRemaining) {
this.lockedTimeRemaining.setValue(lockedTimeRemaining);
registrationState.set(STATE_TIME_REMAINING, lockedTimeRemaining);
}
public void onStartEnterCode() {
canCallAtTime.setValue(System.currentTimeMillis() + FIRST_CALL_AVAILABLE_AFTER_MS);
registrationState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + FIRST_CALL_AVAILABLE_AFTER_MS);
}
public void onCallRequested() {
canCallAtTime.setValue(System.currentTimeMillis() + SUBSEQUENT_CALL_AVAILABLE_AFTER_MS);
private void onCallRequested() {
registrationState.set(STATE_CAN_CALL_AT_TIME, System.currentTimeMillis() + SUBSEQUENT_CALL_AVAILABLE_AFTER_MS);
}
public void setIsReregister(boolean isReregister) {
registrationState.set(STATE_IS_REREGISTER, isReregister);
}
public Single<RequestVerificationCodeResponseProcessor> requestVerificationCode(@NonNull Mode mode) {
String captcha = getCaptchaToken();
clearCaptchaResponse();
if (mode == Mode.PHONE_CALL) {
onCallRequested();
} else if (!getRequestLimiter().canRequest(mode, getNumber().getE164Number(), System.currentTimeMillis())) {
return Single.just(RequestVerificationCodeResponseProcessor.forLocalRateLimit());
}
return verifyAccountRepository.requestVerificationCode(getNumber().getE164Number(),
getRegistrationSecret(),
mode,
captcha)
.map(RequestVerificationCodeResponseProcessor::new)
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(processor -> {
if (processor.hasResult()) {
markASuccessfulAttempt();
setFcmToken(processor.getResult().getFcmToken().orNull());
getRequestLimiter().onSuccessfulRequest(mode, getNumber().getE164Number(), System.currentTimeMillis());
} else {
getRequestLimiter().onUnsuccessfulRequest();
}
updateLimiter();
});
}
public Single<VerifyAccountResponseProcessor> verifyCodeAndRegisterAccountWithoutRegistrationLock(@NonNull String code) {
onVerificationCodeEntered(code);
RegistrationData registrationData = new RegistrationData(getTextCodeEntered(),
getNumber().getE164Number(),
getRegistrationSecret(),
registrationRepository.getRegistrationId(),
registrationRepository.getProfileKey(getNumber().getE164Number()),
getFcmToken());
return verifyAccountRepository.verifyAccount(registrationData)
.map(VerifyAccountResponseWithoutKbs::new)
.flatMap(processor -> {
if (processor.hasResult()) {
return registrationRepository.registerAccountWithoutRegistrationLock(registrationData, processor.getResult())
.map(VerifyAccountResponseWithoutKbs::new);
} else if (processor.registrationLock() && !processor.isKbsLocked()) {
return kbsRepository.getToken(processor.getLockedException().getBasicStorageCredentials())
.map(r -> r.getResult().isPresent() ? new VerifyAccountResponseWithSuccessfulKbs(processor.getResponse(), r.getResult().get())
: new VerifyAccountResponseWithFailedKbs(r));
}
return Single.just(processor);
})
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(processor -> {
if (processor.registrationLock() && !processor.isKbsLocked()) {
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
setKeyBackupTokenData(processor.getTokenData());
} else if (processor.isKbsLocked()) {
setLockedTimeRemaining(processor.getLockedException().getTimeRemaining());
}
});
}
public Single<VerifyCodeWithRegistrationLockResponseProcessor> verifyCodeAndRegisterAccountWithRegistrationLock(@NonNull String pin) {
RegistrationData registrationData = new RegistrationData(getTextCodeEntered(),
getNumber().getE164Number(),
getRegistrationSecret(),
registrationRepository.getRegistrationId(),
registrationRepository.getProfileKey(getNumber().getE164Number()),
getFcmToken());
TokenData kbsTokenData = Objects.requireNonNull(getKeyBackupCurrentToken());
return verifyAccountRepository.verifyAccountWithPin(registrationData, pin, kbsTokenData)
.map(r -> new VerifyCodeWithRegistrationLockResponseProcessor(r, kbsTokenData))
.flatMap(processor -> {
if (processor.hasResult()) {
return registrationRepository.registerAccountWithRegistrationLock(registrationData, processor.getResult(), pin)
.map(processor::updatedIfRegistrationFailed);
} else if (processor.wrongPin()) {
TokenData newToken = TokenData.withResponse(kbsTokenData, processor.getTokenResponse());
return Single.just(new VerifyCodeWithRegistrationLockResponseProcessor(processor.getResponse(), newToken));
}
return Single.just(processor);
})
.observeOn(AndroidSchedulers.mainThread())
.doOnSuccess(processor -> {
if (processor.wrongPin()) {
setKeyBackupTokenData(processor.getToken());
}
});
}
public static final class Factory extends AbstractSavedStateViewModelFactory {
private final boolean isReregister;
public Factory(@NonNull SavedStateRegistryOwner owner, boolean isReregister) {
super(owner, null);
this.isReregister = isReregister;
}
@Override
protected @NonNull <T extends ViewModel> T create(@NonNull String key, @NonNull Class<T> modelClass, @NonNull SavedStateHandle handle) {
//noinspection ConstantConditions
return modelClass.cast(new RegistrationViewModel(handle,
isReregister,
new VerifyAccountRepository(ApplicationDependencies.getApplication()),
new KbsRepository(),
new RegistrationRepository(ApplicationDependencies.getApplication())));
}
}
}

Wyświetl plik

@ -0,0 +1,24 @@
package org.thoughtcrime.securesms.util
import com.dd.CircularProgressButton
object CircularProgressButtonUtil {
@JvmStatic
fun setSpinning(button: CircularProgressButton?) {
button?.apply {
isClickable = false
isIndeterminateProgressMode = true
progress = 50
}
}
@JvmStatic
fun cancelSpinning(button: CircularProgressButton?) {
button?.apply {
progress = 0
isIndeterminateProgressMode = false
isClickable = true
}
}
}

Wyświetl plik

@ -23,6 +23,7 @@ class LifecycleDisposable : DefaultLifecycleObserver {
}
override fun onDestroy(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
disposables.clear()
}
}

Wyświetl plik

@ -8,6 +8,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
public class SignalUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = Log.tag(SignalUncaughtExceptionHandler.class);
@ -20,6 +22,10 @@ public class SignalUncaughtExceptionHandler implements Thread.UncaughtExceptionH
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
if (e instanceof OnErrorNotImplementedException) {
e = e.getCause();
}
Log.e(TAG, "", e, true);
SignalStore.blockUntilAllWritesFinished();
Log.blockUntilAllWritesFinished();

Wyświetl plik

@ -116,14 +116,7 @@
android:id="@+id/countryPickerFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CountryPickerFragment"
android:label="fragment_country_picker"
tools:layout="@layout/fragment_registration_country_picker">
<action
android:id="@+id/action_countrySelected"
app:popUpTo="@id/countryPickerFragment"
app:popUpToInclusive="true" />
</fragment>
tools:layout="@layout/fragment_registration_country_picker"/>
<fragment
android:id="@+id/enterCodeFragment"
@ -217,14 +210,7 @@
android:id="@+id/captchaFragment"
android:name="org.thoughtcrime.securesms.registration.fragments.CaptchaFragment"
android:label="fragment_captcha"
tools:layout="@layout/fragment_registration_captcha">
<action
android:id="@+id/action_captchaComplete"
app:popUpTo="@id/captchaFragment"
app:popUpToInclusive="true" />
</fragment>
tools:layout="@layout/fragment_registration_captcha"/>
<fragment
android:id="@+id/restoreBackupFragment"

Wyświetl plik

@ -1513,6 +1513,7 @@
<string name="RegistrationActivity_signal_needs_access_to_your_contacts_in_order_to_connect_with_friends">Signal needs access to your contacts in order to connect with friends, exchange messages, and make secure calls</string>
<string name="RegistrationActivity_rate_limited_to_service">You\'ve made too many attempts to register this number. Please try again later.</string>
<string name="RegistrationActivity_unable_to_connect_to_service">Unable to connect to service. Please check network connection and try again.</string>
<string name="RegistrationActivity_call_requested">Call requested</string>
<plurals name="RegistrationActivity_debug_log_hint">
<item quantity="one">You are now %d step away from submitting a debug log.</item>
<item quantity="other">You are now %d steps away from submitting a debug log.</item>
@ -3010,6 +3011,7 @@
<string name="RegistrationActivity_call_me_instead_available_in">Call me instead \n (Available in %1$02d:%2$02d)</string>
<string name="RegistrationActivity_contact_signal_support">Contact Signal Support</string>
<string name="RegistrationActivity_code_support_subject">Signal Registration - Verification Code for Android</string>
<string name="RegistrationActivity_incorrect_code">Incorrect code</string>
<string name="BackupUtil_never">Never</string>
<string name="BackupUtil_unknown">Unknown</string>
<string name="preferences_app_protection__see_my_phone_number">See my phone number</string>

Wyświetl plik

@ -1,10 +1,10 @@
package org.thoughtcrime.securesms.registration.viewmodel;
import org.junit.Test;
import org.thoughtcrime.securesms.registration.service.RegistrationCodeRequest;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.thoughtcrime.securesms.registration.VerifyAccountRepository.Mode;
import org.junit.Test;
public final class LocalCodeRequestRateLimiterTest {
@ -12,63 +12,63 @@ public final class LocalCodeRequestRateLimiterTest {
public void initially_can_request() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
}
@Test
public void cant_request_within_same_time_period() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000);
limiter.onSuccessfulRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000);
assertFalse(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000));
assertFalse(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000));
}
@Test
public void can_request_within_same_time_period_if_different_number() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000));
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000);
limiter.onSuccessfulRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+15559874566", 1000 + 59_000));
assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+15559874566", 1000 + 59_000));
}
@Test
public void can_request_within_same_time_period_if_different_mode() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000));
assertTrue(limiter.canRequest(Mode.SMS_WITHOUT_LISTENER, "+155512345678", 1000 + 59_000));
}
@Test
public void can_request_after_time_period() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 60_001));
assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 60_001));
}
@Test
public void can_request_within_same_time_period_if_an_unsuccessful_request_is_seen() {
LocalCodeRequestRateLimiter limiter = new LocalCodeRequestRateLimiter(60_000);
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000));
limiter.onSuccessfulRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
limiter.onSuccessfulRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000);
limiter.onUnsuccessfulRequest();
assertTrue(limiter.canRequest(RegistrationCodeRequest.Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 59_000));
assertTrue(limiter.canRequest(Mode.SMS_WITH_LISTENER, "+155512345678", 1000 + 59_000));
}
}

Wyświetl plik

@ -45,6 +45,7 @@ import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.api.storage.StorageManifestKey;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.contacts.crypto.ContactDiscoveryCipher;
import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
@ -61,6 +62,7 @@ import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
import org.whispersystems.signalservice.internal.push.RemoteConfigResponse;
import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
@ -209,10 +211,14 @@ public class SignalServiceAccountManager {
* @param androidSmsRetrieverSupported
* @param captchaToken If the user has done a CAPTCHA, include this.
* @param challenge If present, it can bypass the CAPTCHA.
* @throws IOException
*/
public void requestSmsVerificationCode(boolean androidSmsRetrieverSupported, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
this.pushServiceSocket.requestSmsVerificationCode(androidSmsRetrieverSupported, captchaToken, challenge);
public ServiceResponse<RequestVerificationCodeResponse> requestSmsVerificationCode(boolean androidSmsRetrieverSupported, Optional<String> captchaToken, Optional<String> challenge, Optional<String> fcmToken) {
try {
this.pushServiceSocket.requestSmsVerificationCode(androidSmsRetrieverSupported, captchaToken, challenge);
return ServiceResponse.forResult(new RequestVerificationCodeResponse(fcmToken), 200, null);
} catch (IOException e) {
return ServiceResponse.forUnknownError(e);
}
}
/**
@ -222,10 +228,14 @@ public class SignalServiceAccountManager {
* @param locale
* @param captchaToken If the user has done a CAPTCHA, include this.
* @param challenge If present, it can bypass the CAPTCHA.
* @throws IOException
*/
public void requestVoiceVerificationCode(Locale locale, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
this.pushServiceSocket.requestVoiceVerificationCode(locale, captchaToken, challenge);
public ServiceResponse<RequestVerificationCodeResponse> requestVoiceVerificationCode(Locale locale, Optional<String> captchaToken, Optional<String> challenge, Optional<String> fcmToken) {
try {
this.pushServiceSocket.requestVoiceVerificationCode(locale, captchaToken, challenge);
return ServiceResponse.forResult(new RequestVerificationCodeResponse(fcmToken), 200, null);
} catch (IOException e) {
return ServiceResponse.forUnknownError(e);
}
}
/**
@ -234,32 +244,76 @@ public class SignalServiceAccountManager {
* @param verificationCode The verification code received via SMS or Voice
* (see {@link #requestSmsVerificationCode} and
* {@link #requestVoiceVerificationCode}).
* @param signalingKey 52 random bytes. A 32 byte AES key and a 20 byte Hmac256 key,
* concatenated.
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
* This value should remain consistent across registrations for the
* same install, but probabilistically differ across registrations
* for separate installs.
* @param pin Deprecated, only supply the pin if you did not find a registrationLock on KBS.
* @return The UUID of the user that was registered.
* @throws IOException for various HTTP and networking errors
*/
public ServiceResponse<VerifyAccountResponse> verifyAccount(String verificationCode,
int signalProtocolRegistrationId,
boolean fetchesMessages,
byte[] unidentifiedAccessKey,
boolean unrestrictedUnidentifiedAccess,
AccountAttributes.Capabilities capabilities,
boolean discoverableByPhoneNumber)
{
try {
VerifyAccountResponse response = this.pushServiceSocket.verifyAccountCode(verificationCode,
null,
signalProtocolRegistrationId,
fetchesMessages,
null,
null,
unidentifiedAccessKey,
unrestrictedUnidentifiedAccess,
capabilities,
discoverableByPhoneNumber);
return ServiceResponse.forResult(response, 200, null);
} catch (IOException e) {
return ServiceResponse.forUnknownError(e);
}
}
/**
* Verify a Signal Service account with a received SMS or voice verification code with
* registration lock.
*
* @param verificationCode The verification code received via SMS or Voice
* (see {@link #requestSmsVerificationCode} and
* {@link #requestVoiceVerificationCode}).
* @param signalProtocolRegistrationId A random 14-bit number that identifies this Signal install.
* This value should remain consistent across registrations for the
* same install, but probabilistically differ across registrations
* for separate installs.
* @param registrationLock Only supply if found on KBS.
* @return The UUID of the user that was registered.
* @throws IOException
*/
public VerifyAccountResponse verifyAccountWithCode(String verificationCode, String signalingKey, int signalProtocolRegistrationId, boolean fetchesMessages,
String pin, String registrationLock,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess,
AccountAttributes.Capabilities capabilities,
boolean discoverableByPhoneNumber)
throws IOException
public ServiceResponse<VerifyAccountResponse> verifyAccountWithRegistrationLockPin(String verificationCode,
int signalProtocolRegistrationId,
boolean fetchesMessages,
String registrationLock,
byte[] unidentifiedAccessKey,
boolean unrestrictedUnidentifiedAccess,
AccountAttributes.Capabilities capabilities,
boolean discoverableByPhoneNumber)
{
return this.pushServiceSocket.verifyAccountCode(verificationCode, signalingKey,
signalProtocolRegistrationId,
fetchesMessages,
pin, registrationLock,
unidentifiedAccessKey,
unrestrictedUnidentifiedAccess,
capabilities,
discoverableByPhoneNumber);
try {
VerifyAccountResponse response = this.pushServiceSocket.verifyAccountCode(verificationCode,
null,
signalProtocolRegistrationId,
fetchesMessages,
null,
registrationLock,
unidentifiedAccessKey,
unrestrictedUnidentifiedAccess,
capabilities,
discoverableByPhoneNumber);
return ServiceResponse.forResult(response, 200, null);
} catch (IOException e) {
return ServiceResponse.forUnknownError(e);
}
}
/**

Wyświetl plik

@ -0,0 +1,8 @@
package org.whispersystems.signalservice.api.push.exceptions;
/**
* Thrown when self limiting networking.
*/
public final class LocalRateLimitException extends Exception {
public LocalRateLimitException() { }
}

Wyświetl plik

@ -3,6 +3,7 @@ package org.whispersystems.signalservice.internal;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse;
import java.util.concurrent.ExecutionException;
@ -92,8 +93,17 @@ public final class ServiceResponse<Result> {
return forUnknownError(throwable.getCause());
} else if (throwable instanceof NonSuccessfulResponseCodeException) {
return forApplicationError(throwable, ((NonSuccessfulResponseCodeException) throwable).getCode(), null);
} else if (throwable instanceof PushNetworkException && throwable.getCause() != null) {
return forUnknownError(throwable.getCause());
} else {
return forExecutionError(throwable);
}
}
public static <T, I> ServiceResponse<T> coerceError(ServiceResponse<I> response) {
if (response.applicationError.isPresent()) {
return ServiceResponse.forApplicationError(response.applicationError.get(), response.status, response.body.orNull());
}
return ServiceResponse.forExecutionError(response.executionError.orNull());
}
}

Wyświetl plik

@ -65,6 +65,10 @@ public abstract class ServiceResponseProcessor<T> {
return response.getStatus() == 401 || response.getStatus() == 403;
}
protected boolean captchaRequired() {
return response.getStatus() == 402;
}
protected boolean notFound() {
return response.getStatus() == 404;
}

Wyświetl plik

@ -0,0 +1,16 @@
package org.whispersystems.signalservice.internal.push;
import org.whispersystems.libsignal.util.guava.Optional;
public final class RequestVerificationCodeResponse {
private final Optional<String> fcmToken;
public RequestVerificationCodeResponse(Optional<String> fcmToken) {
this.fcmToken = fcmToken;
}
public Optional<String> getFcmToken() {
return fcmToken;
}
}

Wyświetl plik

@ -2,6 +2,7 @@ package org.whispersystems.signalservice.internal.websocket;
import org.whispersystems.libsignal.util.guava.Function;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException;
@ -72,6 +73,8 @@ public final class DefaultErrorMapper implements ErrorMapper {
case 401:
case 403:
return new AuthorizationFailedException(status, "Authorization failed!");
case 402:
return new CaptchaRequiredException();
case 404:
return new NotFoundException("Not found");
case 409: