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

@@ -0,0 +1,26 @@
package org.thoughtcrime.securesms.pin;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.KbsEnclave;
import org.thoughtcrime.securesms.util.Util;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public final class KbsEnclaves {
public static @NonNull KbsEnclave current() {
return BuildConfig.KBS_ENCLAVE;
}
public static @NonNull List<KbsEnclave> all() {
return Util.join(Collections.singletonList(BuildConfig.KBS_ENCLAVE), fallbacks());
}
public static @NonNull List<KbsEnclave> fallbacks() {
return Arrays.asList(BuildConfig.KBS_FALLBACKS);
}
}

View File

@@ -1,11 +1,15 @@
package org.thoughtcrime.securesms.pin;
import androidx.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException;
import org.thoughtcrime.securesms.util.Stopwatch;
@@ -21,32 +25,58 @@ import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
class PinRestoreRepository {
public class PinRestoreRepository {
private static final String TAG = Log.tag(PinRestoreRepository.class);
private final Executor executor = SignalExecutors.UNBOUNDED;
private final KeyBackupService kbs = ApplicationDependencies.getKeyBackupService();
private final Executor executor = SignalExecutors.UNBOUNDED;
void getToken(@NonNull Callback<Optional<TokenData>> callback) {
executor.execute(() -> {
try {
String authorization = kbs.getAuthorization();
TokenResponse token = kbs.getToken(authorization);
TokenData tokenData = new TokenData(authorization, token);
callback.onComplete(Optional.of(tokenData));
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.basicAuth, tokenData.tokenResponse);
KbsPinData kbsData = PinState.restoreMasterKey(pin, tokenData.getEnclave(), tokenData.getBasicAuth(), tokenData.getTokenResponse());
PinState.onSignalPinRestore(ApplicationDependencies.getApplication(), Objects.requireNonNull(kbsData), pin);
stopwatch.split("MasterKey");
@@ -64,7 +94,7 @@ class PinRestoreRepository {
} catch (KeyBackupSystemNoDataException e) {
callback.onComplete(new PinResultData(PinResult.LOCKED, tokenData));
} catch (KeyBackupSystemWrongPinException e) {
callback.onComplete(new PinResultData(PinResult.INCORRECT, new TokenData(tokenData.basicAuth, e.getTokenResponse())));
callback.onComplete(new PinResultData(PinResult.INCORRECT, TokenData.withResponse(tokenData, e.getTokenResponse())));
}
});
}
@@ -73,18 +103,81 @@ class PinRestoreRepository {
void onComplete(@NonNull T value);
}
static class TokenData {
public static class TokenData implements Parcelable {
private final KbsEnclave enclave;
private final String basicAuth;
private final TokenResponse tokenResponse;
TokenData(@NonNull String basicAuth, @NonNull TokenResponse tokenResponse) {
TokenData(@NonNull KbsEnclave enclave, @NonNull String basicAuth, @NonNull TokenResponse tokenResponse) {
this.enclave = enclave;
this.basicAuth = basicAuth;
this.tokenResponse = tokenResponse;
}
int getTriesRemaining() {
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 {
@@ -92,7 +185,7 @@ class PinRestoreRepository {
private final TokenData tokenData;
PinResultData(@NonNull PinResult result, @NonNull TokenData tokenData) {
this.result = result;
this.result = result;
this.tokenData = tokenData;
}

View File

@@ -6,8 +6,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.KbsEnclave;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.JobTracker;
import org.thoughtcrime.securesms.jobs.ClearFallbackKbsEnclaveJob;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.jobs.StorageForcePushJob;
import org.thoughtcrime.securesms.keyvalue.KbsValues;
@@ -26,12 +29,14 @@ import org.whispersystems.signalservice.api.KeyBackupServicePinException;
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
import org.whispersystems.signalservice.api.kbs.HashedPin;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
@@ -46,6 +51,7 @@ public final class PinState {
* 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
@@ -58,20 +64,31 @@ public final class PinState {
throw new AssertionError("Cannot restore KBS key, no storage credentials supplied");
}
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
Log.i(TAG, "Preparing to restore from " + enclave.getEnclaveName());
return restoreMasterKeyFromEnclave(enclave, pin, basicStorageCredentials, tokenResponse);
}
Log.i(TAG, "Opening key backup service session");
KeyBackupService.RestoreSession session = keyBackupService.newRegistrationSession(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 e) {
Log.w(TAG, "Failed to restore key", e);
@@ -90,7 +107,7 @@ public final class PinState {
@Nullable String pin,
boolean hasPinToRestore)
{
Log.i(TAG, "onNewRegistration()");
Log.i(TAG, "onRegistration()");
TextSecurePreferences.setV1RegistrationLockPin(context, pin);
@@ -106,7 +123,8 @@ public final class PinState {
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
SignalStore.kbsValues().setKbsMasterKey(kbsData, pin);
SignalStore.pinValues().resetPinReminders();
resetPinRetryCount(context, pin, kbsData);
resetPinRetryCount(context, pin);
ClearFallbackKbsEnclaveJob.clearAll();
} else if (hasPinToRestore) {
Log.i(TAG, "Has a PIN to restore.");
SignalStore.kbsValues().clearRegistrationLockAndPin();
@@ -131,7 +149,8 @@ public final class PinState {
SignalStore.kbsValues().setV2RegistrationLockEnabled(false);
SignalStore.pinValues().resetPinReminders();
SignalStore.storageServiceValues().setNeedsAccountRestore(false);
resetPinRetryCount(context, pin, kbsData);
resetPinRetryCount(context, pin);
ClearFallbackKbsEnclaveJob.clearAll();
updateState(buildInferredStateFromOtherFields());
}
@@ -158,7 +177,7 @@ public final class PinState {
KbsValues kbsValues = SignalStore.kbsValues();
boolean isFirstPin = !kbsValues.hasPin() || kbsValues.hasOptedOut();
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(KbsEnclaves.current());
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession);
KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
@@ -217,7 +236,7 @@ public final class PinState {
assertState(State.PIN_WITH_REGISTRATION_LOCK_DISABLED);
SignalStore.kbsValues().setV2RegistrationLockEnabled(false);
ApplicationDependencies.getKeyBackupService()
ApplicationDependencies.getKeyBackupService(KbsEnclaves.current())
.newPinChangeSession(SignalStore.kbsValues().getRegistrationLockTokenResponse())
.enableRegistrationLock(SignalStore.kbsValues().getOrCreateMasterKey());
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
@@ -240,7 +259,7 @@ public final class PinState {
assertState(State.PIN_WITH_REGISTRATION_LOCK_ENABLED);
SignalStore.kbsValues().setV2RegistrationLockEnabled(true);
ApplicationDependencies.getKeyBackupService()
ApplicationDependencies.getKeyBackupService(KbsEnclaves.current())
.newPinChangeSession(SignalStore.kbsValues().getRegistrationLockTokenResponse())
.disableRegistrationLock();
SignalStore.kbsValues().setV2RegistrationLockEnabled(false);
@@ -259,7 +278,7 @@ public final class PinState {
KbsValues kbsValues = SignalStore.kbsValues();
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService(KbsEnclaves.current());
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession();
HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession);
KbsPinData kbsData = pinChangeSession.setPin(hashedPin, masterKey);
@@ -272,6 +291,22 @@ public final class PinState {
updateState(buildInferredStateFromOtherFields());
}
/**
* Should only be called by {@link org.thoughtcrime.securesms.jobs.KbsEnclaveMigrationWorkerJob}.
*/
@WorkerThread
public static synchronized void onMigrateToNewEnclave(@NonNull String pin)
throws IOException, UnauthenticatedResponseException
{
Log.i(TAG, "onMigrateToNewEnclave()");
assertState(State.PIN_WITH_REGISTRATION_LOCK_DISABLED, State.PIN_WITH_REGISTRATION_LOCK_ENABLED);
Log.i(TAG, "Migrating to enclave " + KbsEnclaves.current().getEnclaveName());
setPinOnEnclave(KbsEnclaves.current(), pin, SignalStore.kbsValues().getOrCreateMasterKey());
ClearFallbackKbsEnclaveJob.clearAll();
}
@WorkerThread
private static void bestEffortRefreshAttributes() {
Optional<JobTracker.JobState> result = ApplicationDependencies.getJobManager().runSynchronously(new RefreshAttributesJob(), TimeUnit.SECONDS.toMillis(10));
@@ -301,23 +336,14 @@ public final class PinState {
}
@WorkerThread
private static void resetPinRetryCount(@NonNull Context context, @Nullable String pin, @NonNull KbsPinData kbsData) {
private static void resetPinRetryCount(@NonNull Context context, @Nullable String pin) {
if (pin == null) {
return;
}
KeyBackupService keyBackupService = ApplicationDependencies.getKeyBackupService();
try {
KbsValues kbsValues = SignalStore.kbsValues();
MasterKey masterKey = kbsValues.getOrCreateMasterKey();
KeyBackupService.PinChangeSession pinChangeSession = keyBackupService.newPinChangeSession(kbsData.getTokenResponse());
HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession);
KbsPinData newData = pinChangeSession.setPin(hashedPin, masterKey);
kbsValues.setKbsMasterKey(newData, pin);
setPinOnEnclave(KbsEnclaves.current(), pin, SignalStore.kbsValues().getOrCreateMasterKey());
TextSecurePreferences.clearRegistrationLockV1(context);
Log.i(TAG, "Pin set/attempts reset on KBS");
} catch (IOException e) {
Log.w(TAG, "May have failed to reset pin attempts!", e);
@@ -326,6 +352,20 @@ public final class PinState {
}
}
@WorkerThread
private static @NonNull KbsPinData setPinOnEnclave(@NonNull KbsEnclave enclave, @NonNull String pin, @NonNull MasterKey masterKey)
throws IOException, UnauthenticatedResponseException
{
KeyBackupService kbs = ApplicationDependencies.getKeyBackupService(enclave);
KeyBackupService.PinChangeSession pinChangeSession = kbs.newPinChangeSession();
HashedPin hashedPin = PinHashing.hashPin(pin, pinChangeSession);
KbsPinData newData = pinChangeSession.setPin(hashedPin, masterKey);
SignalStore.kbsValues().setKbsMasterKey(newData, pin);
return newData;
}
@WorkerThread
private static void optOutOfPin() {
SignalStore.kbsValues().optOut();