mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 08:09:12 +01:00
Clarify networking call order during registration flow.
This commit is contained in:
committed by
Greyson Parrelli
parent
a3d72fc06c
commit
8e32592218
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.thoughtcrime.securesms.pin;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
||||
|
||||
public final class KeyBackupSystemWrongPinException extends Exception {
|
||||
|
||||
private final TokenResponse tokenResponse;
|
||||
|
||||
public KeyBackupSystemWrongPinException(@NonNull TokenResponse tokenResponse){
|
||||
this.tokenResponse = tokenResponse;
|
||||
}
|
||||
|
||||
public @NonNull TokenResponse getTokenResponse() {
|
||||
return tokenResponse;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user