Add support for restoring usernames post-registration.

This commit is contained in:
Greyson Parrelli
2024-01-09 13:01:33 -05:00
parent c16bf65a80
commit 61a4a3b322
11 changed files with 171 additions and 56 deletions

View File

@@ -157,7 +157,6 @@ public final class JobManagerFactories {
put(MultiDeviceVerifiedUpdateJob.KEY, new MultiDeviceVerifiedUpdateJob.Factory());
put(MultiDeviceViewOnceOpenJob.KEY, new MultiDeviceViewOnceOpenJob.Factory());
put(MultiDeviceViewedUpdateJob.KEY, new MultiDeviceViewedUpdateJob.Factory());
put(NewRegistrationUsernameSyncJob.KEY, new NewRegistrationUsernameSyncJob.Factory());
put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory());
put(OptimizeMessageSearchIndexJob.KEY, new OptimizeMessageSearchIndexJob.Factory());
put(PaymentLedgerUpdateJob.KEY, new PaymentLedgerUpdateJob.Factory());
@@ -179,6 +178,7 @@ public final class JobManagerFactories {
put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory());
put(ReactionSendJob.KEY, new ReactionSendJob.Factory());
put(RebuildMessageSearchIndexJob.KEY, new RebuildMessageSearchIndexJob.Factory());
put(ReclaimUsernameAndLinkJob.KEY, new ReclaimUsernameAndLinkJob.Factory());
put(RefreshAttributesJob.KEY, new RefreshAttributesJob.Factory());
put(RefreshCallLinkDetailsJob.KEY, new RefreshCallLinkDetailsJob.Factory());
put(RefreshSvrCredentialsJob.KEY, new RefreshSvrCredentialsJob.Factory());
@@ -309,6 +309,7 @@ public final class JobManagerFactories {
put("SmsReceiveJob", new FailingJob.Factory());
put("StoryReadStateMigrationJob", new PassingMigrationJob.Factory());
put("GroupV1MigrationJob", new FailingJob.Factory());
put("NewRegistrationUsernameSyncJob", new FailingJob.Factory());
}};
}

View File

@@ -1,47 +0,0 @@
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import java.io.IOException
/**
* If a user registers and the storage sync service doesn't contain a username,
* then we should delete our username from the server.
*/
class NewRegistrationUsernameSyncJob private constructor(parameters: Parameters) : BaseJob(parameters) {
companion object {
private val TAG = Log.tag(NewRegistrationUsernameSyncJob::class.java)
const val KEY = "NewRegistrationUsernameSyncJob"
}
constructor() : this(
Parameters.Builder()
.setQueue(StorageSyncJob.QUEUE_KEY)
.setMaxInstancesForFactory(1)
.addConstraint(NetworkConstraint.KEY)
.build()
)
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY
override fun onFailure() = Unit
override fun onRun() {
RefreshOwnProfileJob.checkUsernameIsInSync()
}
override fun onShouldRetry(e: Exception): Boolean {
return e is IOException
}
class Factory : Job.Factory<NewRegistrationUsernameSyncJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): NewRegistrationUsernameSyncJob {
return NewRegistrationUsernameSyncJob(parameters)
}
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
import org.thoughtcrime.securesms.util.FeatureFlags
import kotlin.time.Duration.Companion.days
class ReclaimUsernameAndLinkJob private constructor(parameters: Job.Parameters) : Job(parameters) {
companion object {
const val KEY = "UsernameAndLinkRestoreJob"
private val TAG = Log.tag(ReclaimUsernameAndLinkJob::class.java)
}
constructor() : this(
Parameters.Builder()
.setQueue(StorageSyncJob.QUEUE_KEY)
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(Parameters.UNLIMITED)
.setLifespan(30.days.inWholeMilliseconds)
.setMaxInstancesForFactory(1)
.build()
)
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY
override fun run(): Result {
return when (UsernameRepository.reclaimUsernameIfNecessary()) {
UsernameRepository.UsernameReclaimResult.SUCCESS -> Result.success()
UsernameRepository.UsernameReclaimResult.PERMANENT_ERROR -> Result.success()
UsernameRepository.UsernameReclaimResult.NETWORK_ERROR -> Result.retry(BackoffUtil.exponentialBackoff(runAttempt + 1, FeatureFlags.getDefaultMaxBackoff()))
}
}
override fun onFailure() = Unit
class Factory : Job.Factory<ReclaimUsernameAndLinkJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): ReclaimUsernameAndLinkJob {
return ReclaimUsernameAndLinkJob(parameters)
}
}
}

View File

@@ -296,7 +296,7 @@ public class RefreshOwnProfileJob extends BaseJob {
.enqueue();
}
static void checkUsernameIsInSync() {
private static void checkUsernameIsInSync() {
boolean validated = false;
try {

View File

@@ -4,13 +4,17 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.signal.libsignal.usernames.Username;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.JobTracker;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.AccountValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
@@ -111,6 +115,16 @@ public class StorageAccountRestoreJob extends BaseJob {
SignalDatabase.getRawDatabase().endTransaction();
}
// We will try to reclaim the username here, as early as possible, but the registration flow also enqueues a username restore job,
// so failing here isn't a huge deal
if (SignalStore.account().getUsername() != null) {
Log.i(TAG, "Attempting to reclaim username...");
UsernameRepository.UsernameReclaimResult result = UsernameRepository.reclaimUsernameIfNecessary();
Log.i(TAG, "Username reclaim result: " + result.name());
} else {
Log.i(TAG, "No username to reclaim.");
}
JobManager jobManager = ApplicationDependencies.getJobManager();
if (accountRecord.getAvatarUrlPath().isPresent()) {

View File

@@ -39,6 +39,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
private static final String LAST_CONSISTENCY_CHECK_TIME = "misc.last_consistency_check_time";
private static final String SERVER_TIME_OFFSET = "misc.server_time_offset";
private static final String LAST_SERVER_TIME_OFFSET_UPDATE = "misc.last_server_time_offset_update";
private static final String NEEDS_USERNAME_RESTORE = "misc.needs_username_restore";
MiscellaneousValues(@NonNull KeyValueStore store) {
super(store);
@@ -47,6 +48,7 @@ public final class MiscellaneousValues extends SignalStoreValues {
@Override
void onFirstEverAppLaunch() {
putLong(MESSAGE_REQUEST_ENABLE_TIME, 0);
putBoolean(NEEDS_USERNAME_RESTORE, true);
}
@Override
@@ -331,4 +333,15 @@ public final class MiscellaneousValues extends SignalStoreValues {
public long getLastKnownServerTimeOffsetUpdateTime() {
return getLong(LAST_SERVER_TIME_OFFSET_UPDATE, 0);
}
/**
* Whether or not we should attempt to restore the user's username and link.
*/
public boolean needsUsernameRestore() {
return getBoolean(NEEDS_USERNAME_RESTORE, false);
}
public void setNeedsUsernameRestore(boolean value) {
putBoolean(NEEDS_USERNAME_RESTORE, value);
}
}

View File

@@ -12,7 +12,7 @@ import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.jobs.NewRegistrationUsernameSyncJob
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
import org.thoughtcrime.securesms.jobs.ResetSvrGuessCountJob
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob
@@ -152,7 +152,7 @@ object SvrRepository {
ApplicationDependencies
.getJobManager()
.startChain(StorageSyncJob())
.then(NewRegistrationUsernameSyncJob())
.then(ReclaimUsernameAndLinkJob())
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10))
stopwatch.split("contact-restore")

View File

@@ -121,7 +121,52 @@ object UsernameRepository {
}
/**
* Deletes the username and username link from the local user's account
* Attempts to reclaim the username that is currently stored on disk if necessary.
* This is intended to be used after registration.
*
* This method call may result in mutating [SignalStore] state.
*/
@WorkerThread
@JvmStatic
fun reclaimUsernameIfNecessary(): UsernameReclaimResult {
if (!SignalStore.misc().needsUsernameRestore()) {
Log.d(TAG, "[reclaimUsernameIfNecessary] No need to restore username. Skipping.")
return UsernameReclaimResult.SUCCESS
}
val username = SignalStore.account().username
val link = SignalStore.account().usernameLink
if (username == null || link == null) {
Log.d(TAG, "[reclaimUsernameIfNecessary] No username or link to restore. Skipping.")
SignalStore.misc().setNeedsUsernameRestore(false)
return UsernameReclaimResult.SUCCESS
}
val result = reclaimUsernameIfNecessaryInternal(Username(username), link)
when (result) {
UsernameReclaimResult.SUCCESS -> {
Log.i(TAG, "[reclaimUsernameIfNecessary] Successfully reclaimed username and link.")
SignalStore.misc().setNeedsUsernameRestore(false)
}
UsernameReclaimResult.PERMANENT_ERROR -> {
Log.w(TAG, "[reclaimUsernameIfNecessary] Permanently failed to reclaim username and link. User will see an error.")
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED
SignalStore.misc().setNeedsUsernameRestore(false)
}
UsernameReclaimResult.NETWORK_ERROR -> {
Log.w(TAG, "[reclaimUsernameIfNecessary] Hit a transient network error while trying to reclaim username and link.")
}
}
return result
}
/**
* Deletes the username from the local user's account
*/
@JvmStatic
fun deleteUsernameAndLink(): Single<UsernameDeleteResult> {
@@ -418,10 +463,36 @@ object UsernameRepository {
}
}
@WorkerThread
@JvmStatic
private fun reclaimUsernameIfNecessaryInternal(username: Username, usernameLinkComponents: UsernameLinkComponents): UsernameReclaimResult {
try {
accountManager.reclaimUsernameAndLink(username, usernameLinkComponents)
} catch (e: UsernameTakenException) {
Log.w(TAG, "[reclaimUsername] Username gone.")
return UsernameReclaimResult.PERMANENT_ERROR
} catch (e: UsernameIsNotReservedException) {
Log.w(TAG, "[reclaimUsername] Username was not reserved.")
return UsernameReclaimResult.PERMANENT_ERROR
} catch (e: BaseUsernameException) {
Log.w(TAG, "[reclaimUsername] Invalid username.")
return UsernameReclaimResult.PERMANENT_ERROR
} catch (e: IOException) {
Log.w(TAG, "[reclaimUsername] Network error.", e)
return UsernameReclaimResult.NETWORK_ERROR
}
return UsernameReclaimResult.SUCCESS
}
enum class UsernameSetResult {
SUCCESS, USERNAME_UNAVAILABLE, USERNAME_INVALID, NETWORK_ERROR, CANDIDATE_GENERATION_ERROR
}
enum class UsernameReclaimResult {
SUCCESS, PERMANENT_ERROR, NETWORK_ERROR
}
enum class UsernameDeleteResult {
SUCCESS, NETWORK_ERROR
}

View File

@@ -8,7 +8,7 @@ import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.NewRegistrationUsernameSyncJob;
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob;
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -56,7 +56,7 @@ public final class RegistrationLockFragment extends BaseRegistrationLockFragment
ApplicationDependencies
.getJobManager()
.startChain(new StorageSyncJob())
.then(new NewRegistrationUsernameSyncJob())
.then(new ReclaimUsernameAndLinkJob())
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10));
stopwatch.split("ContactRestore");

View File

@@ -12,7 +12,7 @@ import androidx.savedstate.SavedStateRegistryOwner;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.NewRegistrationUsernameSyncJob;
import org.thoughtcrime.securesms.jobs.ReclaimUsernameAndLinkJob;
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -402,7 +402,7 @@ public final class RegistrationViewModel extends BaseRegistrationViewModel {
ApplicationDependencies
.getJobManager()
.startChain(new StorageSyncJob())
.then(new NewRegistrationUsernameSyncJob())
.then(new ReclaimUsernameAndLinkJob())
.enqueueAndBlockUntilCompletion(TimeUnit.SECONDS.toMillis(10));
stopwatch.split("ContactRestore");

View File

@@ -794,6 +794,17 @@ public class SignalServiceAccountManager {
}
}
public UsernameLinkComponents reclaimUsernameAndLink(Username username, UsernameLinkComponents linkComponents) throws IOException {
try {
UsernameLink link = username.generateLink(linkComponents.getEntropy());
UUID serverId = this.pushServiceSocket.confirmUsernameAndCreateNewLink(username, link);
return new UsernameLinkComponents(link.getEntropy(), serverId);
} catch (BaseUsernameException e) {
throw new AssertionError(e);
}
}
public UsernameLinkComponents updateUsernameLink(UsernameLink newUsernameLink) throws IOException {
UUID serverId = this.pushServiceSocket.createUsernameLink(Base64.encodeUrlSafeWithoutPadding(newUsernameLink.getEncryptedUsername()), true);