Add the ability to migrate to new KBS enclaves.
parent
e22384b6b4
commit
474963dcf1
|
@ -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=\""
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ClearFallbackKbsEnclaveJob> {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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<KbsEnclaveMigrationWorkerJob> {
|
||||
@Override
|
||||
public @NonNull KbsEnclaveMigrationWorkerJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new KbsEnclaveMigrationWorkerJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<KbsEnclaveMigrationJob> {
|
||||
@Override
|
||||
public @NonNull KbsEnclaveMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new KbsEnclaveMigrationJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -116,6 +116,18 @@ public class Util {
|
|||
return join(boxed, delimeter);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static @NonNull <E> List<E> join(@NonNull List<E>... lists) {
|
||||
int totalSize = Stream.of(lists).reduce(0, (sum, list) -> sum + list.size());
|
||||
List<E> joined = new ArrayList<>(totalSize);
|
||||
|
||||
for (List<E> list : lists) {
|
||||
joined.addAll(list);
|
||||
}
|
||||
|
||||
return joined;
|
||||
}
|
||||
|
||||
public static String join(List<Long> list, String delimeter) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -2,6 +2,6 @@ package org.whispersystems.signalservice.api;
|
|||
|
||||
public final class KeyBackupSystemNoDataException extends Exception {
|
||||
|
||||
KeyBackupSystemNoDataException() {
|
||||
public KeyBackupSystemNoDataException() {
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue