Use a default authenticator name until the user sets one.

This commit is contained in:
Greyson Parrelli
2026-09-15 18:26:04 -04:00
parent 878933ecef
commit c5d648c46a
3 changed files with 17 additions and 9 deletions
@@ -17,11 +17,13 @@ import org.signal.libsignal.net.TooManyMfaKeysException
import org.signal.libsignal.net.TooManyTotpKeysException
import org.signal.libsignal.net.TotpParameters
import org.signal.network.api.AccountApiV2
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.util.RemoteConfig
import java.net.URLEncoder
import java.time.Instant
import org.signal.appsettings.R as AppSettingsR
/**
* Everything the authenticator app screens need, sitting between them and the TOTP endpoints on [AccountApiV2].
@@ -33,7 +35,8 @@ import java.time.Instant
class TotpRepository(
private val api: AccountApiV2 = SignalNetwork.accountApiV2,
private val masterKeyProvider: () -> MasterKey = { SignalStore.svr.masterKey },
private val clock: () -> Long = System::currentTimeMillis
private val clock: () -> Long = System::currentTimeMillis,
private val defaultAppName: () -> String = { AppDependencies.application.getString(AppSettingsR.string.TotpRepository__authenticator) }
) {
companion object {
@@ -111,14 +114,14 @@ class TotpRepository(
/**
* Confirms the pending key with a code from the user's authenticator app.
*
* The key is confirmed without a name, because the service wants metadata at confirmation time and the user doesn't
* name their app until the screen after this one. Naming it later means a brief window where a key has no name, which
* is a better failure than a window where the second factor isn't active yet.
* The key is confirmed with a default name, because the service wants metadata at confirmation time and the user
* doesn't name their app until the screen after this one. Anything that lists keys in that window shows the default
* rather than a nameless entry.
*/
suspend fun confirmPendingApp(code: String): ConfirmResult {
val oneTimePassword = code.toIntOrNull() ?: return ConfirmResult.IncorrectCode
val metadata = MfaMetadata(name = "", createdAt = Instant.ofEpochMilli(clock()))
val metadata = MfaMetadata(name = defaultAppName(), createdAt = Instant.ofEpochMilli(clock()))
return when (val result = api.confirmTotpKey(oneTimePassword = oneTimePassword, metadata = metadata, masterKey = masterKeyProvider())) {
is RequestResult.Success -> {
@@ -179,7 +182,7 @@ class TotpRepository(
return setMetadata(app.id, MfaMetadata(name = name, createdAt = Instant.ofEpochMilli(app.createdAt)))
}
/** Names a newly confirmed app, which was confirmed without one moments ago. */
/** Names a newly confirmed app, replacing the default name it was confirmed with moments ago. */
suspend fun nameNewTotpApp(appId: Long, name: String): UpdateResult {
return setMetadata(appId, MfaMetadata(name = name, createdAt = Instant.ofEpochMilli(clock())))
}
@@ -46,6 +46,7 @@ class TotpRepositoryTest {
private const val NOW = 1_700_000_000_000L
private const val ACCOUNT_NAME = "8B4A1F0C"
private const val CODE = "123456"
private const val DEFAULT_NAME = "Authenticator"
private const val KEY_ID = 1
private val KEY = ByteArray(32) { it.toByte() }
@@ -58,7 +59,7 @@ class TotpRepositoryTest {
private var now = NOW
private val api = mockk<AccountApiV2>()
private val repository = TotpRepository(api = api, masterKeyProvider = { MASTER_KEY }, clock = { now })
private val repository = TotpRepository(api = api, masterKeyProvider = { MASTER_KEY }, clock = { now }, defaultAppName = { DEFAULT_NAME })
@Before
fun setUp() {
@@ -181,13 +182,13 @@ class TotpRepositoryTest {
/** The service wants metadata at confirmation time, and the user hasn't been asked for a name yet. */
@Test
fun `a key is confirmed without a name, stamped with the time it was confirmed`() = runTest {
fun `a key is confirmed with a default name, stamped with the time it was confirmed`() = runTest {
val metadata = slot<MfaMetadata>()
coEvery { api.confirmTotpKey(any(), capture(metadata), any()) } returns RequestResult.Success(KEY_ID)
repository.confirmPendingApp(CODE)
assertThat(metadata.captured.name).isEqualTo("")
assertThat(metadata.captured.name).isEqualTo(DEFAULT_NAME)
assertThat(metadata.captured.createdAt).isEqualTo(Instant.ofEpochMilli(NOW))
}
@@ -146,6 +146,10 @@
<string name="TotpCodeEntryScreen__incorrect_code">Incorrect code. Enter the code showing in your authenticator app now.</string>
<!-- Error shown under the code field when we couldn\'t reach the service to check the code -->
<string name="TotpCodeEntryScreen__couldnt_reach_signal">Couldn\'t reach Signal. Check your connection and try again.</string>
<!-- Default name given to an authenticator app the moment it\'s set up, before the user chooses their own name for it -->
<string name="TotpRepository__authenticator">Authenticator</string>
<!-- TotpNameEntryScreen -->
<!-- Title of the screen where the user names an authenticator app -->
<string name="TotpNameEntryScreen__choose_a_name">Choose a name</string>