From 474963dcf1468f0449fb0ee24155d6a02c9f4d37 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 5 Oct 2020 09:26:51 -0400 Subject: [PATCH] Add the ability to migrate to new KBS enclaves. --- app/build.gradle | 13 +- .../thoughtcrime/securesms/KbsEnclave.java | 49 +++++++ .../dependencies/ApplicationDependencies.java | 10 +- .../jobs/ClearFallbackKbsEnclaveJob.java | 102 +++++++++++++++ .../securesms/jobs/JobManagerFactories.java | 4 + .../jobs/KbsEnclaveMigrationWorkerJob.java | 85 ++++++++++++ .../securesms/keyvalue/KbsValues.java | 4 + .../migrations/KbsEnclaveMigrationJob.java | 53 ++++++++ .../securesms/pin/KbsEnclaves.java | 26 ++++ .../securesms/pin/PinRestoreRepository.java | 123 +++++++++++++++--- .../thoughtcrime/securesms/pin/PinState.java | 82 +++++++++--- .../fragments/EnterCodeFragment.java | 11 +- .../fragments/RegistrationLockFragment.java | 20 ++- .../service/CodeVerificationRequest.java | 52 ++++---- .../service/RegistrationService.java | 6 +- .../viewmodel/RegistrationViewModel.java | 31 +---- .../org/thoughtcrime/securesms/util/Util.java | 12 ++ .../signalservice/api/KeyBackupService.java | 19 +++ .../api/KeyBackupSystemNoDataException.java | 2 +- 19 files changed, 588 insertions(+), 116 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/KbsEnclave.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/ClearFallbackKbsEnclaveJob.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/KbsEnclaveMigrationWorkerJob.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/migrations/KbsEnclaveMigrationJob.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/pin/KbsEnclaves.java diff --git a/app/build.gradle b/app/build.gradle index 8021c22019..3e05e4c87a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -132,9 +132,10 @@ android { buildConfigField "int", "CONTENT_PROXY_PORT", "443" buildConfigField "String", "SIGNAL_AGENT", "\"OWA\"" buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\"" - buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"" - buildConfigField "String", "KBS_SERVICE_ID", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"" - buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\"" + buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," + + "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " + + "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"; + buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"" buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\"" buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' @@ -214,8 +215,10 @@ android { buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"" buildConfigField "String", "CDS_MRENCLAVE", "\"bd123560b01c8fa92935bc5ae15cd2064e5c45215f23f0bd40364d521329d2ad\"" - buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\"" - buildConfigField "String", "KBS_SERVICE_ID", "\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\"" + buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " + + "\"038c40bbbacdc873caa81ac793bb75afde6dfe436a99ab1f15e3f0cbb7434ced\", " + + "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")" + buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"" buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\"" } diff --git a/app/src/main/java/org/thoughtcrime/securesms/KbsEnclave.java b/app/src/main/java/org/thoughtcrime/securesms/KbsEnclave.java new file mode 100644 index 0000000000..aa3afa40dc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/KbsEnclave.java @@ -0,0 +1,49 @@ +package org.thoughtcrime.securesms; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +/** + * Used in our {@link BuildConfig} to tie together the various attributes of a KBS instance. This + * is sitting in the root directory so it can be accessed by the build config. + */ +public final class KbsEnclave { + + private final String enclaveName; + private final String serviceId; + private final String mrEnclave; + + public KbsEnclave(@NonNull String enclaveName, @NonNull String serviceId, @NonNull String mrEnclave) { + this.enclaveName = enclaveName; + this.serviceId = serviceId; + this.mrEnclave = mrEnclave; + } + + public @NonNull String getMrEnclave() { + return mrEnclave; + } + + public @NonNull String getEnclaveName() { + return enclaveName; + } + + public @NonNull String getServiceId() { + return serviceId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + KbsEnclave enclave = (KbsEnclave) o; + return enclaveName.equals(enclave.enclaveName) && + serviceId.equals(enclave.serviceId) && + mrEnclave.equals(enclave.mrEnclave); + } + + @Override + public int hashCode() { + return Objects.hash(enclaveName, serviceId, mrEnclave); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index a4adbec662..024b114cd9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -6,6 +6,7 @@ import androidx.annotation.MainThread; import androidx.annotation.NonNull; import org.thoughtcrime.securesms.BuildConfig; +import org.thoughtcrime.securesms.KbsEnclave; import org.thoughtcrime.securesms.messages.IncomingMessageProcessor; import org.thoughtcrime.securesms.messages.BackgroundMessageRetriever; import org.thoughtcrime.securesms.groups.GroupsV2AuthorizationMemoryValueCache; @@ -15,6 +16,7 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueStore; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.megaphone.MegaphoneRepository; import org.thoughtcrime.securesms.notifications.MessageNotifier; +import org.thoughtcrime.securesms.pin.KbsEnclaves; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.messages.IncomingMessageObserver; @@ -111,11 +113,11 @@ public class ApplicationDependencies { return groupsV2Operations; } - public static synchronized @NonNull KeyBackupService getKeyBackupService() { + public static synchronized @NonNull KeyBackupService getKeyBackupService(@NonNull KbsEnclave enclave) { return getSignalServiceAccountManager().getKeyBackupService(IasKeyStore.getIasKeyStore(application), - BuildConfig.KBS_ENCLAVE_NAME, - Hex.fromStringOrThrow(BuildConfig.KBS_SERVICE_ID), - BuildConfig.KBS_MRENCLAVE, + enclave.getEnclaveName(), + Hex.fromStringOrThrow(enclave.getServiceId()), + enclave.getMrEnclave(), 10); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ClearFallbackKbsEnclaveJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ClearFallbackKbsEnclaveJob.java new file mode 100644 index 0000000000..834f63f5a9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ClearFallbackKbsEnclaveJob.java @@ -0,0 +1,102 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.KbsEnclave; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.pin.KbsEnclaves; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * Clears data from an old KBS enclave. + */ +public class ClearFallbackKbsEnclaveJob extends BaseJob { + + public static final String KEY = "ClearFallbackKbsEnclaveJob"; + + private static final String TAG = Log.tag(ClearFallbackKbsEnclaveJob.class); + + private static final String KEY_ENCLAVE_NAME = "enclaveName"; + private static final String KEY_SERVICE_ID = "serviceId"; + private static final String KEY_MR_ENCLAVE = "mrEnclave"; + + private final KbsEnclave enclave; + + ClearFallbackKbsEnclaveJob(@NonNull KbsEnclave enclave) { + this(new Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(90)) + .setMaxAttempts(Parameters.UNLIMITED) + .setQueue("ClearFallbackKbsEnclaveJob") + .build(), + enclave); + } + + public static void clearAll() { + if (KbsEnclaves.fallbacks().isEmpty()) { + Log.i(TAG, "No fallbacks!"); + return; + } + + JobManager jobManager = ApplicationDependencies.getJobManager(); + + for (KbsEnclave enclave : KbsEnclaves.fallbacks()) { + jobManager.add(new ClearFallbackKbsEnclaveJob(enclave)); + } + } + + private ClearFallbackKbsEnclaveJob(@NonNull Parameters parameters, @NonNull KbsEnclave enclave) { + super(parameters); + this.enclave = enclave; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_ENCLAVE_NAME, enclave.getEnclaveName()) + .putString(KEY_SERVICE_ID, enclave.getServiceId()) + .putString(KEY_MR_ENCLAVE, enclave.getMrEnclave()) + .build(); + } + + @Override + public void onRun() throws IOException, UnauthenticatedResponseException { + Log.i(TAG, "Preparing to delete data from " + enclave.getEnclaveName()); + ApplicationDependencies.getKeyBackupService(enclave).newPinChangeSession().removePin(); + Log.i(TAG, "Successfully deleted the data from " + enclave.getEnclaveName()); + } + + @Override + public boolean onShouldRetry(@NonNull Exception e) { + return true; + } + + @Override + public void onFailure() { + throw new AssertionError("This job should never fail. " + getClass().getSimpleName()); + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull ClearFallbackKbsEnclaveJob create(@NonNull Parameters parameters, @NonNull Data data) { + KbsEnclave enclave = new KbsEnclave(data.getString(KEY_ENCLAVE_NAME), + data.getString(KEY_SERVICE_ID), + data.getString(KEY_MR_ENCLAVE)); + + return new ClearFallbackKbsEnclaveJob(parameters, enclave); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 7409d68569..8b85b83e8a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob; import org.thoughtcrime.securesms.migrations.AvatarMigrationJob; import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob; import org.thoughtcrime.securesms.migrations.DatabaseMigrationJob; +import org.thoughtcrime.securesms.migrations.KbsEnclaveMigrationJob; import org.thoughtcrime.securesms.migrations.LegacyMigrationJob; import org.thoughtcrime.securesms.migrations.MigrationCompleteJob; import org.thoughtcrime.securesms.migrations.PassingMigrationJob; @@ -62,9 +63,11 @@ public final class JobManagerFactories { put(AvatarGroupsV1DownloadJob.KEY, new AvatarGroupsV1DownloadJob.Factory()); put(AvatarGroupsV2DownloadJob.KEY, new AvatarGroupsV2DownloadJob.Factory()); put(CleanPreKeysJob.KEY, new CleanPreKeysJob.Factory()); + put(ClearFallbackKbsEnclaveJob.KEY, new ClearFallbackKbsEnclaveJob.Factory()); put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory()); put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory()); put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory()); + put(KbsEnclaveMigrationWorkerJob.KEY, new KbsEnclaveMigrationWorkerJob.Factory()); put(LeaveGroupJob.KEY, new LeaveGroupJob.Factory()); put(LocalBackupJob.KEY, new LocalBackupJob.Factory()); put(MmsDownloadJob.KEY, new MmsDownloadJob.Factory()); @@ -132,6 +135,7 @@ public final class JobManagerFactories { put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory()); put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory()); put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory()); + put(KbsEnclaveMigrationJob.KEY, new KbsEnclaveMigrationJob.Factory()); put(LegacyMigrationJob.KEY, new LegacyMigrationJob.Factory()); put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory()); put(PinOptOutMigration.KEY, new PinOptOutMigration.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/KbsEnclaveMigrationWorkerJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/KbsEnclaveMigrationWorkerJob.java new file mode 100644 index 0000000000..c9c046e1c3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/KbsEnclaveMigrationWorkerJob.java @@ -0,0 +1,85 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.migrations.KbsEnclaveMigrationJob; +import org.thoughtcrime.securesms.pin.PinState; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; + +import java.io.IOException; + +/** + * Should only be enqueued by {@link KbsEnclaveMigrationJob}. Does the actual work of migrating KBS + * data to the new enclave and deleting it from the old enclave(s). + */ +public class KbsEnclaveMigrationWorkerJob extends BaseJob { + + public static final String KEY = "KbsEnclaveMigrationWorkerJob"; + + private static final String TAG = Log.tag(KbsEnclaveMigrationWorkerJob.class); + + public KbsEnclaveMigrationWorkerJob() { + this(new Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setLifespan(Parameters.IMMORTAL) + .setMaxAttempts(Parameters.UNLIMITED) + .setQueue("KbsEnclaveMigrationWorkerJob") + .setMaxInstances(1) + .build()); + } + + private KbsEnclaveMigrationWorkerJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public @NonNull Data serialize() { + return Data.EMPTY; + } + + @Override + public void onRun() throws IOException, UnauthenticatedResponseException { + String pin = SignalStore.kbsValues().getPin(); + + if (SignalStore.kbsValues().hasOptedOut()) { + Log.w(TAG, "Opted out of KBS! Nothing to migrate."); + return; + } + + if (pin == null) { + Log.w(TAG, "No PIN available! Can't migrate!"); + return; + } + + PinState.onMigrateToNewEnclave(pin); + Log.i(TAG, "Migration successful!"); + } + + @Override + public boolean onShouldRetry(@NonNull Exception e) { + return e instanceof IOException || + e instanceof UnauthenticatedResponseException; + } + + @Override + public void onFailure() { + throw new AssertionError("This job should never fail. " + getClass().getSimpleName()); + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull KbsEnclaveMigrationWorkerJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new KbsEnclaveMigrationWorkerJob(parameters); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java index e2fb451069..8dd450f2c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KbsValues.java @@ -137,6 +137,10 @@ public final class KbsValues extends SignalStoreValues { } } + public synchronized @Nullable String getPin() { + return getString(PIN, null); + } + public synchronized @Nullable String getLocalPinHash() { return getString(LOCK_LOCAL_PIN_HASH, null); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/KbsEnclaveMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/KbsEnclaveMigrationJob.java new file mode 100644 index 0000000000..dff47afb70 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/KbsEnclaveMigrationJob.java @@ -0,0 +1,53 @@ +package org.thoughtcrime.securesms.migrations; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobs.KbsEnclaveMigrationWorkerJob; + +/** + * A job to be run whenever we add a new KBS enclave. In order to prevent this moderately-expensive + * task from blocking the network for too long, this task simply enqueues another non-migration job, + * {@link KbsEnclaveMigrationWorkerJob}, to do the heavy lifting. + */ +public class KbsEnclaveMigrationJob extends MigrationJob { + + public static final String KEY = "KbsEnclaveMigrationJob"; + + KbsEnclaveMigrationJob() { + this(new Parameters.Builder().build()); + } + + private KbsEnclaveMigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + public boolean isUiBlocking() { + return false; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void performMigration() { + ApplicationDependencies.getJobManager().add(new KbsEnclaveMigrationWorkerJob()); + } + + @Override + boolean shouldRetry(@NonNull Exception e) { + return false; + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull KbsEnclaveMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new KbsEnclaveMigrationJob(parameters); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/KbsEnclaves.java b/app/src/main/java/org/thoughtcrime/securesms/pin/KbsEnclaves.java new file mode 100644 index 0000000000..df0d209ed7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/KbsEnclaves.java @@ -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 all() { + return Util.join(Collections.singletonList(BuildConfig.KBS_ENCLAVE), fallbacks()); + } + + public static @NonNull List fallbacks() { + return Arrays.asList(BuildConfig.KBS_FALLBACKS); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java index 5ac818d12d..7b19c9d4b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java @@ -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> 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 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 CREATOR = new Creator() { + @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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java index 20faacb49d..0c9cbd5201 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java @@ -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 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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java index 5c6d933205..23337840be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/EnterCodeFragment.java @@ -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() { @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"); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java index 35e4b65594..c48cc6cf51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java @@ -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."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java index c6088ba432..2bebb0a1ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/CodeVerificationRequest.java @@ -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() { 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. *

* 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. diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationService.java b/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationService.java index 29ce4052b4..d89a0f90f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/service/RegistrationService.java @@ -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); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java index c3894dea31..b851d95fb1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/viewmodel/RegistrationViewModel.java @@ -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 textCodeEntered; private final MutableLiveData captchaToken; private final MutableLiveData fcmToken; - private final MutableLiveData basicStorageCredentials; private final MutableLiveData restoreFlowShown; private final MutableLiveData successfulCodeRequestAttempts; private final MutableLiveData requestLimiter; - private final MutableLiveData keyBackupCurrentTokenJson; + private final MutableLiveData kbsTokenData; private final MutableLiveData lockedTimeRemaining; private final MutableLiveData 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 getLockedTimeRemaining() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index 86b0b444db..45895e1d4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -116,6 +116,18 @@ public class Util { return join(boxed, delimeter); } + @SafeVarargs + public static @NonNull List join(@NonNull List... lists) { + int totalSize = Stream.of(lists).reduce(0, (sum, list) -> sum + list.size()); + List joined = new ArrayList<>(totalSize); + + for (List list : lists) { + joined.addAll(list); + } + + return joined; + } + public static String join(List list, String delimeter) { StringBuilder sb = new StringBuilder(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java index 58a23426f6..b9aafc559e 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupService.java @@ -17,6 +17,7 @@ import org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse import org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse; import org.whispersystems.signalservice.internal.push.PushServiceSocket; import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil; +import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; import java.io.IOException; @@ -215,6 +216,21 @@ public final class KeyBackupService { return new KbsPinData(masterKey, tokenResponse); } + @Override + public void removePin() + throws IOException, UnauthenticatedResponseException + { + try { + RemoteAttestation remoteAttestation = getAndVerifyRemoteAttestation(); + KeyBackupRequest request = KeyBackupCipher.createKeyDeleteRequest(currentToken, remoteAttestation, serviceId); + KeyBackupResponse response = pushServiceSocket.putKbsData(authorization, request, remoteAttestation.getCookies(), enclaveName); + + KeyBackupCipher.getKeyDeleteResponseStatus(response, remoteAttestation); + } catch (InvalidCiphertextException e) { + throw new UnauthenticatedResponseException(e); + } + } + @Override public void enableRegistrationLock(MasterKey masterKey) throws IOException { pushServiceSocket.setRegistrationLockV2(masterKey.deriveRegistrationLock()); @@ -266,6 +282,9 @@ public final class KeyBackupService { /** Creates a PIN. Does nothing to registration lock. */ KbsPinData setPin(HashedPin hashedPin, MasterKey masterKey) throws IOException, UnauthenticatedResponseException; + /** Removes the PIN data from KBS. */ + void removePin() throws IOException, UnauthenticatedResponseException; + /** Enables registration lock. This assumes a PIN is set. */ void enableRegistrationLock(MasterKey masterKey) throws IOException; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupSystemNoDataException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupSystemNoDataException.java index 5b60f07c4e..a49bc73690 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupSystemNoDataException.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/KeyBackupSystemNoDataException.java @@ -2,6 +2,6 @@ package org.whispersystems.signalservice.api; public final class KeyBackupSystemNoDataException extends Exception { - KeyBackupSystemNoDataException() { + public KeyBackupSystemNoDataException() { } }