Add the ability to migrate to new KBS enclaves.

This commit is contained in:
Greyson Parrelli
2020-10-05 09:26:51 -04:00
committed by Alan Evans
parent e22384b6b4
commit 474963dcf1
19 changed files with 588 additions and 116 deletions

View File

@@ -22,6 +22,7 @@ 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.logging.Log;
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;
@@ -30,7 +31,6 @@ import org.thoughtcrime.securesms.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.util.ArrayList;
import java.util.Collections;
@@ -107,7 +107,7 @@ public final class EnterCodeFragment extends BaseRegistrationFragment {
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
registrationService.verifyAccount(requireActivity(), model.getFcmToken(), code, null, null, null,
registrationService.verifyAccount(requireActivity(), model.getFcmToken(), code, null, null,
new CodeVerificationRequest.VerifyCallback() {
@Override
@@ -133,10 +133,9 @@ public final class EnterCodeFragment extends BaseRegistrationFragment {
}
@Override
public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenResponse tokenResponse, @NonNull String kbsStorageCredentials) {
public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull PinRestoreRepository.TokenData tokenData, @NonNull String kbsStorageCredentials) {
model.setLockedTimeRemaining(timeRemaining);
model.setStorageCredentials(kbsStorageCredentials);
model.setKeyBackupCurrentToken(tokenResponse);
model.setKeyBackupTokenData(tokenData);
keyboard.displayLocked().addListener(new AssertedSuccessListener<Boolean>() {
@Override
public void onSuccess(Boolean r) {
@@ -147,7 +146,7 @@ public final class EnterCodeFragment extends BaseRegistrationFragment {
}
@Override
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse tokenResponse) {
public void onIncorrectKbsRegistrationLockPin(@NonNull PinRestoreRepository.TokenData tokenData) {
throw new AssertionError("Unexpected, user has made no pin guesses");
}

View File

@@ -25,12 +25,12 @@ import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType;
import org.thoughtcrime.securesms.logging.Log;
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.registration.viewmodel.RegistrationViewModel;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.util.concurrent.TimeUnit;
@@ -106,10 +106,10 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
getModel().getLockedTimeRemaining()
.observe(getViewLifecycleOwner(), t -> timeRemaining = t);
TokenResponse keyBackupCurrentToken = getModel().getKeyBackupCurrentToken();
TokenData keyBackupCurrentToken = getModel().getKeyBackupCurrentToken();
if (keyBackupCurrentToken != null) {
int triesRemaining = keyBackupCurrentToken.getTries();
int triesRemaining = keyBackupCurrentToken.getTriesRemaining();
if (triesRemaining <= 3) {
int daysRemaining = getLockoutDays(timeRemaining);
@@ -158,8 +158,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
RegistrationViewModel model = getModel();
RegistrationService registrationService = RegistrationService.getInstance(model.getNumber().getE164Number(), model.getRegistrationSecret());
TokenResponse tokenResponse = model.getKeyBackupCurrentToken();
String basicStorageCredentials = model.getBasicStorageCredentials();
TokenData tokenData = model.getKeyBackupCurrentToken();
setSpinning(pinButton);
@@ -167,8 +166,7 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
model.getFcmToken(),
model.getTextCodeEntered(),
pin,
basicStorageCredentials,
tokenResponse,
tokenData,
new CodeVerificationRequest.VerifyCallback() {
@@ -189,19 +187,19 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment {
}
@Override
public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenResponse kbsTokenResponse, @NonNull String kbsStorageCredentials) {
public void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenData kbsTokenData, @NonNull String kbsStorageCredentials) {
throw new AssertionError("Not expected after a pin guess");
}
@Override
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenResponse tokenResponse) {
public void onIncorrectKbsRegistrationLockPin(@NonNull TokenData tokenData) {
cancelSpinning(pinButton);
pinEntry.getText().clear();
enableAndFocusPinEntry();
model.setKeyBackupCurrentToken(tokenResponse);
model.setKeyBackupTokenData(tokenData);
int triesRemaining = tokenResponse.getTries();
int triesRemaining = tokenData.getTriesRemaining();
if (triesRemaining == 0) {
Log.w(TAG, "Account locked. User out of attempts on KBS.");

View File

@@ -22,6 +22,8 @@ import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
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;
@@ -65,41 +67,40 @@ public final class CodeVerificationRequest {
/**
* 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 kbsTokenResponse 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.
* @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 String basicStorageCredentials,
@Nullable TokenResponse kbsTokenResponse,
@Nullable TokenData kbsTokenData,
@NonNull VerifyCallback callback)
{
new AsyncTask<Void, Void, Result>() {
private volatile LockedException lockedException;
private volatile TokenResponse kbsToken;
private volatile TokenData tokenData;
@Override
protected Result doInBackground(Void... voids) {
final boolean pinSupplied = pin != null;
final boolean tryKbs = kbsTokenResponse != null;
final boolean tryKbs = tokenData != null;
try {
kbsToken = kbsTokenResponse;
verifyAccount(context, credentials, code, pin, kbsTokenResponse, basicStorageCredentials, fcmToken);
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) {
kbsToken = e.getTokenResponse();
tokenData = TokenData.withResponse(tokenData, e.getTokenResponse());
return Result.KBS_WRONG_PIN;
} catch (LockedException e) {
if (pinSupplied && tryKbs) {
@@ -110,8 +111,8 @@ public final class CodeVerificationRequest {
lockedException = e;
if (e.getBasicStorageCredentials() != null) {
try {
kbsToken = getToken(e.getBasicStorageCredentials());
if (kbsToken == null || kbsToken.getTries() == 0) {
tokenData = getToken(e.getBasicStorageCredentials());
if (tokenData == null || tokenData.getTriesRemaining() == 0) {
return Result.KBS_ACCOUNT_LOCKED;
}
} catch (IOException ex) {
@@ -137,12 +138,12 @@ public final class CodeVerificationRequest {
callback.onSuccessfulRegistration();
break;
case PIN_LOCKED:
if (kbsToken != null) {
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(), kbsToken, lockedException.getBasicStorageCredentials());
callback.onKbsRegistrationLockPinRequired(lockedException.getTimeRemaining(), tokenData, lockedException.getBasicStorageCredentials());
} else {
Log.w(TAG, "Reg Locked: V1 pin needed for registration");
callback.onV1RegistrationLockPinRequiredOrIncorrect(lockedException.getTimeRemaining());
@@ -156,7 +157,7 @@ public final class CodeVerificationRequest {
break;
case KBS_WRONG_PIN:
Log.w(TAG, "KBS Pin was wrong");
callback.onIncorrectKbsRegistrationLockPin(kbsToken);
callback.onIncorrectKbsRegistrationLockPin(tokenData);
break;
case KBS_ACCOUNT_LOCKED:
Log.w(TAG, "KBS Account is locked");
@@ -167,9 +168,9 @@ public final class CodeVerificationRequest {
}.executeOnExecutor(SignalExecutors.UNBOUNDED);
}
private static TokenResponse getToken(@Nullable String basicStorageCredentials) throws IOException {
private static TokenData getToken(@Nullable String basicStorageCredentials) throws IOException {
if (basicStorageCredentials == null) return null;
return ApplicationDependencies.getKeyBackupService().getToken(basicStorageCredentials);
return new PinRestoreRepository().getTokenSync(basicStorageCredentials);
}
private static void handleSuccessfulRegistration(@NonNull Context context) {
@@ -185,12 +186,11 @@ public final class CodeVerificationRequest {
@NonNull Credentials credentials,
@NonNull String code,
@Nullable String pin,
@Nullable TokenResponse kbsTokenResponse,
@Nullable String kbsStorageCredentials,
@Nullable TokenData kbsTokenData,
@Nullable String fcmToken)
throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException
{
boolean isV2RegistrationLock = kbsTokenResponse != null;
boolean isV2RegistrationLock = kbsTokenData != null;
int registrationId = KeyHelper.generateRegistrationId(false);
boolean universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);
ProfileKey profileKey = findExistingProfileKey(context, credentials.getE164number());
@@ -206,7 +206,7 @@ public final class CodeVerificationRequest {
SessionUtil.archiveAllSessions(context);
SignalServiceAccountManager accountManager = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());
KbsPinData kbsData = isV2RegistrationLock ? PinState.restoreMasterKey(pin, kbsStorageCredentials, kbsTokenResponse) : null;
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;
@@ -292,14 +292,14 @@ public final class CodeVerificationRequest {
/**
* The account is locked with a V2 (KBS) pin. Called before any user pin guesses.
*/
void onKbsRegistrationLockPinRequired(long timeRemaining, @NonNull TokenResponse kbsTokenResponse, @NonNull String kbsStorageCredentials);
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 TokenResponse kbsTokenResponse);
void onIncorrectKbsRegistrationLockPin(@NonNull TokenData kbsTokenResponse);
/**
* V2 (KBS) pin is set, but there is no data on KBS.

View File

@@ -5,6 +5,7 @@ import android.app.Activity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.pin.PinRestoreRepository;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.io.IOException;
@@ -39,10 +40,9 @@ public final class RegistrationService {
@Nullable String fcmToken,
@NonNull String code,
@Nullable String pin,
@Nullable String basicStorageCredentials,
@Nullable TokenResponse tokenResponse,
@Nullable PinRestoreRepository.TokenData tokenData,
@NonNull CodeVerificationRequest.VerifyCallback callback)
{
CodeVerificationRequest.verifyAccount(activity, credentials, fcmToken, code, pin, basicStorageCredentials, tokenResponse, callback);
CodeVerificationRequest.verifyAccount(activity, credentials, fcmToken, code, pin, tokenData, callback);
}
}

View File

@@ -9,6 +9,7 @@ import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.pin.PinRestoreRepository.TokenData;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.util.JsonUtil;
@@ -28,11 +29,10 @@ public final class RegistrationViewModel extends ViewModel {
private final MutableLiveData<String> textCodeEntered;
private final MutableLiveData<String> captchaToken;
private final MutableLiveData<String> fcmToken;
private final MutableLiveData<String> basicStorageCredentials;
private final MutableLiveData<Boolean> restoreFlowShown;
private final MutableLiveData<Integer> successfulCodeRequestAttempts;
private final MutableLiveData<LocalCodeRequestRateLimiter> requestLimiter;
private final MutableLiveData<String> keyBackupCurrentTokenJson;
private final MutableLiveData<TokenData> kbsTokenData;
private final MutableLiveData<Long> lockedTimeRemaining;
private final MutableLiveData<Long> canCallAtTime;
@@ -43,11 +43,10 @@ public final class RegistrationViewModel extends ViewModel {
textCodeEntered = savedStateHandle.getLiveData("TEXT_CODE_ENTERED", "");
captchaToken = savedStateHandle.getLiveData("CAPTCHA");
fcmToken = savedStateHandle.getLiveData("FCM_TOKEN");
basicStorageCredentials = savedStateHandle.getLiveData("BASIC_STORAGE_CREDENTIALS");
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));
keyBackupCurrentTokenJson = savedStateHandle.getLiveData("KBS_TOKEN");
kbsTokenData = savedStateHandle.getLiveData("KBS_TOKEN");
lockedTimeRemaining = savedStateHandle.getLiveData("TIME_REMAINING", 0L);
canCallAtTime = savedStateHandle.getLiveData("CAN_CALL_AT_TIME", 0L);
}
@@ -158,28 +157,12 @@ public final class RegistrationViewModel extends ViewModel {
requestLimiter.setValue(requestLimiter.getValue());
}
public void setStorageCredentials(@Nullable String storageCredentials) {
basicStorageCredentials.setValue(storageCredentials);
public @Nullable TokenData getKeyBackupCurrentToken() {
return kbsTokenData.getValue();
}
public @Nullable String getBasicStorageCredentials() {
return basicStorageCredentials.getValue();
}
public @Nullable TokenResponse getKeyBackupCurrentToken() {
String json = keyBackupCurrentTokenJson.getValue();
if (json == null) return null;
try {
return JsonUtil.fromJson(json, TokenResponse.class);
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
public void setKeyBackupCurrentToken(TokenResponse tokenResponse) {
String json = tokenResponse == null ? null : JsonUtil.toJson(tokenResponse);
keyBackupCurrentTokenJson.setValue(json);
public void setKeyBackupTokenData(TokenData tokenData) {
kbsTokenData.setValue(tokenData);
}
public LiveData<Long> getLockedTimeRemaining() {