Only mark username corrupted after repeated failures.

This commit is contained in:
Greyson Parrelli
2023-11-16 22:54:19 -05:00
committed by Cody Henthorne
parent 1aa7175006
commit 8023285b9d
5 changed files with 68 additions and 25 deletions

View File

@@ -12,7 +12,6 @@ import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.AppUtil import org.signal.core.util.AppUtil
import org.signal.core.util.Hex
import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.concurrent.SimpleTask import org.signal.core.util.concurrent.SimpleTask
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
@@ -717,7 +716,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
.setTitle("Corrupt your username?") .setTitle("Corrupt your username?")
.setMessage("Are you sure? You might not be able to get your original username back.") .setMessage("Are you sure? You might not be able to get your original username back.")
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
val random = "${Hex.toStringCondensed(Util.getSecretBytes(4))}.${Random.nextInt(1, 100)}" val random = "${(1..5).map { ('a'..'z').random() }.joinToString(separator = "") }.${Random.nextInt(1, 100)}"
SignalStore.account().username = random SignalStore.account().username = random
SignalDatabase.recipients.setUsername(Recipient.self().id, random) SignalDatabase.recipients.setUsername(Recipient.self().id, random)

View File

@@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.AccountValues; import org.thoughtcrime.securesms.keyvalue.AccountValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.subscription.Subscriber; import org.thoughtcrime.securesms.subscription.Subscriber;
@@ -267,9 +268,10 @@ public class RefreshOwnProfileJob extends BaseJob {
if (TextUtils.isEmpty(localUsernameHash) && TextUtils.isEmpty(remoteUsernameHash)) { if (TextUtils.isEmpty(localUsernameHash) && TextUtils.isEmpty(remoteUsernameHash)) {
Log.d(TAG, "Local and remote username hash are both empty. Considering validated."); Log.d(TAG, "Local and remote username hash are both empty. Considering validated.");
UsernameRepository.onUsernameConsistencyValidated();
} else if (!Objects.equals(localUsernameHash, remoteUsernameHash)) { } else if (!Objects.equals(localUsernameHash, remoteUsernameHash)) {
Log.w(TAG, "Local username hash does not match server username hash. Local hash: " + (TextUtils.isEmpty(localUsername) ? "empty" : "present") + ", Remote hash: " + (TextUtils.isEmpty(remoteUsernameHash) ? "empty" : "present")); Log.w(TAG, "Local username hash does not match server username hash. Local hash: " + (TextUtils.isEmpty(localUsername) ? "empty" : "present") + ", Remote hash: " + (TextUtils.isEmpty(remoteUsernameHash) ? "empty" : "present"));
SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED); UsernameRepository.onUsernameMismatchDetected();
return; return;
} else { } else {
Log.d(TAG, "Username validated."); Log.d(TAG, "Username validated.");
@@ -278,7 +280,8 @@ public class RefreshOwnProfileJob extends BaseJob {
Log.w(TAG, "Failed perform synchronization check during username phase.", e); Log.w(TAG, "Failed perform synchronization check during username phase.", e);
} catch (BaseUsernameException e) { } catch (BaseUsernameException e) {
Log.w(TAG, "Our local username data is invalid!", e); Log.w(TAG, "Our local username data is invalid!", e);
SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED); UsernameRepository.onUsernameMismatchDetected();
return;
} }
try { try {
@@ -291,9 +294,7 @@ public class RefreshOwnProfileJob extends BaseJob {
if (!remoteUsername.getUsername().equals(SignalStore.account().getUsername())) { if (!remoteUsername.getUsername().equals(SignalStore.account().getUsername())) {
Log.w(TAG, "The remote username decrypted ok, but the decrypted username did not match our local username!"); Log.w(TAG, "The remote username decrypted ok, but the decrypted username did not match our local username!");
SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.LINK_CORRUPTED); UsernameRepository.onUsernameLinkMismatchDetected();
SignalStore.account().setUsernameLink(null);
StorageSyncHelper.scheduleSyncForDataChange();
} else { } else {
Log.d(TAG, "Username link validated."); Log.d(TAG, "Username link validated.");
} }
@@ -304,13 +305,11 @@ public class RefreshOwnProfileJob extends BaseJob {
Log.w(TAG, "Failed perform synchronization check during the username link phase.", e); Log.w(TAG, "Failed perform synchronization check during the username link phase.", e);
} catch (BaseUsernameException e) { } catch (BaseUsernameException e) {
Log.w(TAG, "Failed to decrypt username link using the remote encrypted username and our local entropy!", e); Log.w(TAG, "Failed to decrypt username link using the remote encrypted username and our local entropy!", e);
SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.LINK_CORRUPTED); UsernameRepository.onUsernameLinkMismatchDetected();
SignalStore.account().setUsernameLink(null);
StorageSyncHelper.scheduleSyncForDataChange();
} }
if (validated) { if (validated) {
SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.IN_SYNC); UsernameRepository.onUsernameConsistencyValidated();
} }
} }

View File

@@ -6,7 +6,6 @@ import android.content.SharedPreferences
import android.preference.PreferenceManager import android.preference.PreferenceManager
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import org.signal.core.util.Base64 import org.signal.core.util.Base64
import org.signal.core.util.LongSerializer
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.IdentityKeyPair
@@ -72,6 +71,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
private const val KEY_USERNAME_LINK_ENTROPY = "account.username_link_entropy" private const val KEY_USERNAME_LINK_ENTROPY = "account.username_link_entropy"
private const val KEY_USERNAME_LINK_SERVER_ID = "account.username_link_server_id" private const val KEY_USERNAME_LINK_SERVER_ID = "account.username_link_server_id"
private const val KEY_USERNAME_SYNC_STATE = "phoneNumberPrivacy.usernameSyncState" private const val KEY_USERNAME_SYNC_STATE = "phoneNumberPrivacy.usernameSyncState"
private const val KEY_USERNAME_SYNC_ERROR_COUNT = "phoneNumberPrivacy.usernameErrorCount"
@VisibleForTesting @VisibleForTesting
const val KEY_E164 = "account.e164" const val KEY_E164 = "account.e164"
@@ -397,17 +397,14 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
* There are some cases where our username may fall out of sync with the service. In particular, we may get a new value for our username from * There are some cases where our username may fall out of sync with the service. In particular, we may get a new value for our username from
* storage service but then find that it doesn't match what's on the service. * storage service but then find that it doesn't match what's on the service.
*/ */
var usernameSyncState: UsernameSyncState by enumValue( var usernameSyncState: UsernameSyncState
KEY_USERNAME_SYNC_STATE, get() = UsernameSyncState.deserialize(getLong(KEY_USERNAME_SYNC_STATE, UsernameSyncState.IN_SYNC.serialize()))
UsernameSyncState.IN_SYNC, set(value) {
object : LongSerializer<UsernameSyncState> { Log.i(TAG, "Marking username sync state as: $value")
override fun serialize(data: UsernameSyncState): Long { putLong(KEY_USERNAME_SYNC_STATE, value.serialize())
Log.i(TAG, "Marking username sync state as: $data")
return data.serialize()
} }
override fun deserialize(data: Long): UsernameSyncState = UsernameSyncState.deserialize(data)
} var usernameSyncErrorCount: Int by integerValue(KEY_USERNAME_SYNC_ERROR_COUNT, 0)
)
private fun clearLocalCredentials() { private fun clearLocalCredentials() {
putString(KEY_SERVICE_PASSWORD, Util.getSecret(18)) putString(KEY_SERVICE_PASSWORD, Util.getSecret(18))
@@ -520,7 +517,7 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
companion object { companion object {
fun deserialize(value: Long): UsernameSyncState { fun deserialize(value: Long): UsernameSyncState {
return values().firstOrNull { it.value == value } ?: throw IllegalArgumentException("Invalud value: $value") return values().firstOrNull { it.value == value } ?: throw IllegalArgumentException("Invalid value: $value")
} }
} }
} }

View File

@@ -83,7 +83,8 @@ object UsernameRepository {
private val URL_REGEX = """(https://)?signal.me/?#eu/([a-zA-Z0-9+\-_/]+)""".toRegex() private val URL_REGEX = """(https://)?signal.me/?#eu/([a-zA-Z0-9+\-_/]+)""".toRegex()
val BASE_URL = "https://signal.me/#eu/" private const val BASE_URL = "https://signal.me/#eu/"
private const val USERNAME_SYNC_ERROR_THRESHOLD = 3
private val accountManager: SignalServiceAccountManager get() = ApplicationDependencies.getSignalServiceAccountManager() private val accountManager: SignalServiceAccountManager get() = ApplicationDependencies.getSignalServiceAccountManager()
@@ -153,6 +154,7 @@ object UsernameRepository {
if (SignalStore.account().usernameSyncState == AccountValues.UsernameSyncState.LINK_CORRUPTED) { if (SignalStore.account().usernameSyncState == AccountValues.UsernameSyncState.LINK_CORRUPTED) {
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
SignalStore.account().usernameSyncErrorCount = 0
} }
SignalDatabase.recipients.markNeedsSync(Recipient.self().id) SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
@@ -251,6 +253,44 @@ object UsernameRepository {
return BASE_URL + base64 return BASE_URL + base64
} }
@JvmStatic
fun onUsernameConsistencyValidated() {
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
if (SignalStore.account().usernameSyncErrorCount > 0) {
Log.i(TAG, "Username consistency validated. There were previously ${SignalStore.account().usernameSyncErrorCount} error(s).")
SignalStore.account().usernameSyncErrorCount = 0
}
}
@JvmStatic
fun onUsernameMismatchDetected() {
SignalStore.account().usernameSyncErrorCount++
if (SignalStore.account().usernameSyncErrorCount >= USERNAME_SYNC_ERROR_THRESHOLD) {
Log.w(TAG, "We've now seen ${SignalStore.account().usernameSyncErrorCount} mismatches in a row. Marking username and link as corrupted.")
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED
SignalStore.account().usernameSyncErrorCount = 0
} else {
Log.w(TAG, "Username mismatch reported. At ${SignalStore.account().usernameSyncErrorCount} / $USERNAME_SYNC_ERROR_THRESHOLD tries.")
}
}
@JvmStatic
fun onUsernameLinkMismatchDetected() {
SignalStore.account().usernameSyncErrorCount++
if (SignalStore.account().usernameSyncErrorCount >= USERNAME_SYNC_ERROR_THRESHOLD) {
Log.w(TAG, "We've now seen ${SignalStore.account().usernameSyncErrorCount} mismatches in a row. Marking link as corrupted.")
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.LINK_CORRUPTED
SignalStore.account().usernameLink = null
SignalStore.account().usernameSyncErrorCount = 0
StorageSyncHelper.scheduleSyncForDataChange()
} else {
Log.w(TAG, "Link mismatch reported. At ${SignalStore.account().usernameSyncErrorCount} / $USERNAME_SYNC_ERROR_THRESHOLD tries.")
}
}
@WorkerThread @WorkerThread
private fun reserveUsernameInternal(nickname: String): Result<UsernameState.Reserved, UsernameSetResult> { private fun reserveUsernameInternal(nickname: String): Result<UsernameState.Reserved, UsernameSetResult> {
return try { return try {
@@ -293,6 +333,7 @@ object UsernameRepository {
SignalStore.account().usernameLink = null SignalStore.account().usernameLink = null
SignalDatabase.recipients.setUsername(Recipient.self().id, reserved.username) SignalDatabase.recipients.setUsername(Recipient.self().id, reserved.username)
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
SignalStore.account().usernameSyncErrorCount = 0
SignalDatabase.recipients.markNeedsSync(Recipient.self().id) SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange() StorageSyncHelper.scheduleSyncForDataChange()
@@ -345,6 +386,7 @@ object UsernameRepository {
SignalStore.account().username = null SignalStore.account().username = null
SignalStore.account().usernameLink = null SignalStore.account().usernameLink = null
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
SignalStore.account().usernameSyncErrorCount = 0
SignalDatabase.recipients.markNeedsSync(Recipient.self().id) SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange() StorageSyncHelper.scheduleSyncForDataChange()
Log.i(TAG, "[deleteUsername] Successfully deleted the username.") Log.i(TAG, "[deleteUsername] Successfully deleted the username.")

View File

@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.model.RecipientRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.AccountValues;
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.payments.Entropy; import org.thoughtcrime.securesms.payments.Entropy;
@@ -207,7 +208,6 @@ public final class StorageSyncHelper {
SignalStore.storyValues().setUserHasViewedOnboardingStory(update.getNew().hasViewedOnboardingStory()); SignalStore.storyValues().setUserHasViewedOnboardingStory(update.getNew().hasViewedOnboardingStory());
SignalStore.storyValues().setFeatureDisabled(update.getNew().isStoriesDisabled()); SignalStore.storyValues().setFeatureDisabled(update.getNew().isStoriesDisabled());
SignalStore.storyValues().setUserHasSeenGroupStoryEducationSheet(update.getNew().hasSeenGroupStoryEducationSheet()); SignalStore.storyValues().setUserHasSeenGroupStoryEducationSheet(update.getNew().hasSeenGroupStoryEducationSheet());
SignalStore.account().setUsername(update.getNew().getUsername());
if (update.getNew().getStoryViewReceiptsState() == OptionalBool.UNSET) { if (update.getNew().getStoryViewReceiptsState() == OptionalBool.UNSET) {
SignalStore.storyValues().setViewedReceiptsEnabled(update.getNew().isReadReceiptsEnabled()); SignalStore.storyValues().setViewedReceiptsEnabled(update.getNew().isReadReceiptsEnabled());
@@ -236,6 +236,12 @@ public final class StorageSyncHelper {
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(self, update.getNew().getAvatarUrlPath().get())); ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(self, update.getNew().getAvatarUrlPath().get()));
} }
if (!update.getNew().getUsername().equals(update.getOld().getUsername())) {
SignalStore.account().setUsername(update.getNew().getUsername());
SignalStore.account().setUsernameSyncState(AccountValues.UsernameSyncState.IN_SYNC);
SignalStore.account().setUsernameSyncErrorCount(0);
}
if (update.getNew().getUsernameLink() != null) { if (update.getNew().getUsernameLink() != null) {
SignalStore.account().setUsernameLink( SignalStore.account().setUsernameLink(
new UsernameLinkComponents( new UsernameLinkComponents(