Update username validation to use libsignal.

This commit is contained in:
Greyson Parrelli
2024-02-20 17:00:09 -05:00
parent 67c3f41dff
commit 23d6a71a3b
5 changed files with 145 additions and 70 deletions

View File

@@ -182,7 +182,7 @@ public class UsernameEditFragment extends LoggingFragment {
case DISCRIMINATOR_TOO_LONG -> getString(R.string.UsernameEditFragment__invalid_username_enter_a_maximum_of_d_digits, UsernameUtil.MAX_DISCRIMINATOR_LENGTH);
case DISCRIMINATOR_TOO_SHORT -> getString(R.string.UsernameEditFragment__invalid_username_enter_a_minimum_of_d_digits, UsernameUtil.MIN_DISCRIMINATOR_LENGTH);
case DISCRIMINATOR_CANNOT_BE_00 -> getString(R.string.UsernameEditFragment__this_number_cant_be_00);
case DISCRIMINATOR_CANNOT_START_WITH_00 -> getString(R.string.UsernameEditFragment__this_number_cant_start_with_00);
case DISCRIMINATOR_CANNOT_START_WITH_0 -> getString(R.string.UsernameEditFragment__this_number_cant_start_with_0);
};
int colorRes = error != null ? R.color.signal_colorError : R.color.signal_colorPrimary;

View File

@@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameSet
import org.thoughtcrime.securesms.util.NetworkUtil
import org.thoughtcrime.securesms.util.UsernameUtil.InvalidReason
import org.thoughtcrime.securesms.util.UsernameUtil.checkDiscriminator
import org.thoughtcrime.securesms.util.UsernameUtil.checkUsername
import org.thoughtcrime.securesms.util.UsernameUtil.checkNickname
import org.thoughtcrime.securesms.util.rx.RxStore
import org.whispersystems.signalservice.api.util.Usernames
import java.util.concurrent.TimeUnit
@@ -159,7 +159,7 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern
return
}
val invalidReason: InvalidReason? = checkUsername(usernameState.getNickname())
val invalidReason: InvalidReason? = checkNickname(usernameState.getNickname())
if (invalidReason != null) {
Log.w(TAG, "Username was submitted, but did not pass validity checks. Reason: $invalidReason")
uiState.update { it.copy(buttonState = ButtonState.SUBMIT_DISABLED, usernameStatus = mapNicknameError(invalidReason)) }
@@ -257,7 +257,7 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern
return
}
val invalidReason: InvalidReason? = checkUsername(nickname)
val invalidReason: InvalidReason? = checkNickname(nickname)
if (invalidReason != null) {
uiState.update { uiState ->
uiState.copy(
@@ -366,7 +366,7 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern
DISCRIMINATOR_TOO_LONG,
DISCRIMINATOR_HAS_INVALID_CHARACTERS,
DISCRIMINATOR_CANNOT_BE_00,
DISCRIMINATOR_CANNOT_START_WITH_00
DISCRIMINATOR_CANNOT_START_WITH_0
}
enum class ButtonState {
@@ -394,7 +394,9 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern
InvalidReason.TOO_LONG -> UsernameStatus.TOO_LONG
InvalidReason.STARTS_WITH_NUMBER -> UsernameStatus.CANNOT_START_WITH_NUMBER
InvalidReason.INVALID_CHARACTERS -> UsernameStatus.INVALID_CHARACTERS
InvalidReason.INVALID_NUMBER, InvalidReason.INVALID_NUMBER_PREFIX -> error("Unexpected reason $invalidReason")
InvalidReason.INVALID_NUMBER,
InvalidReason.INVALID_NUMBER_00,
InvalidReason.INVALID_NUMBER_PREFIX_0 -> error("Unexpected reason $invalidReason")
}
}
@@ -403,8 +405,8 @@ internal class UsernameEditViewModel private constructor(private val mode: Usern
InvalidReason.TOO_SHORT -> UsernameStatus.DISCRIMINATOR_TOO_SHORT
InvalidReason.TOO_LONG -> UsernameStatus.DISCRIMINATOR_TOO_LONG
InvalidReason.INVALID_CHARACTERS -> UsernameStatus.DISCRIMINATOR_HAS_INVALID_CHARACTERS
InvalidReason.INVALID_NUMBER -> UsernameStatus.DISCRIMINATOR_CANNOT_BE_00
InvalidReason.INVALID_NUMBER_PREFIX -> UsernameStatus.DISCRIMINATOR_CANNOT_START_WITH_00
InvalidReason.INVALID_NUMBER_00 -> UsernameStatus.DISCRIMINATOR_CANNOT_BE_00
InvalidReason.INVALID_NUMBER_PREFIX_0 -> UsernameStatus.DISCRIMINATOR_CANNOT_START_WITH_0
else -> UsernameStatus.INVALID_GENERIC
}
}

View File

@@ -1,6 +1,19 @@
package org.thoughtcrime.securesms.util
import org.signal.core.util.logging.Log
import org.signal.libsignal.usernames.BadDiscriminatorCharacterException
import org.signal.libsignal.usernames.BadNicknameCharacterException
import org.signal.libsignal.usernames.BaseUsernameException
import org.signal.libsignal.usernames.CannotBeEmptyException
import org.signal.libsignal.usernames.CannotStartWithDigitException
import org.signal.libsignal.usernames.DiscriminatorCannotBeEmptyException
import org.signal.libsignal.usernames.DiscriminatorCannotBeSingleDigitException
import org.signal.libsignal.usernames.DiscriminatorCannotBeZeroException
import org.signal.libsignal.usernames.DiscriminatorCannotHaveLeadingZerosException
import org.signal.libsignal.usernames.DiscriminatorTooLargeException
import org.signal.libsignal.usernames.NicknameTooLongException
import org.signal.libsignal.usernames.NicknameTooShortException
import org.signal.libsignal.usernames.Username
import java.util.Locale
import java.util.regex.Pattern
@@ -36,52 +49,75 @@ object UsernameUtil {
}
@JvmStatic
fun checkUsername(value: String?): InvalidReason? {
return when {
value == null -> {
InvalidReason.TOO_SHORT
}
value.length < MIN_NICKNAME_LENGTH -> {
InvalidReason.TOO_SHORT
}
value.length > MAX_NICKNAME_LENGTH -> {
InvalidReason.TOO_LONG
}
DIGIT_START_PATTERN.matcher(value).matches() -> {
InvalidReason.STARTS_WITH_NUMBER
}
!FULL_PATTERN.matcher(value).matches() -> {
InvalidReason.INVALID_CHARACTERS
}
else -> {
null
}
fun checkNickname(value: String?): InvalidReason? {
if (value == null) {
return InvalidReason.TOO_SHORT
}
return try {
// We only want to check the nickname, so we pass in a known-valid discriminator
Username.fromParts(value, "01", MIN_NICKNAME_LENGTH, MAX_NICKNAME_LENGTH)
null
} catch (e: BadNicknameCharacterException) {
InvalidReason.INVALID_CHARACTERS
} catch (e: CannotBeEmptyException) {
InvalidReason.TOO_SHORT
} catch (e: CannotStartWithDigitException) {
InvalidReason.STARTS_WITH_NUMBER
} catch (e: NicknameTooLongException) {
InvalidReason.TOO_LONG
} catch (e: NicknameTooShortException) {
InvalidReason.TOO_SHORT
} catch (e: BaseUsernameException) {
Log.w(TAG, "Unhandled verification exception!", e)
InvalidReason.INVALID_CHARACTERS
}
}
fun checkDiscriminator(value: String?): InvalidReason? {
return when {
value == null -> {
null
}
value == "00" -> {
InvalidReason.INVALID_NUMBER
}
value.startsWith("00") -> {
InvalidReason.INVALID_NUMBER_PREFIX
}
value.length < MIN_DISCRIMINATOR_LENGTH -> {
if (value == null) {
return null
}
if (value.length < MIN_DISCRIMINATOR_LENGTH) {
return InvalidReason.TOO_SHORT
}
if (value.length > MAX_DISCRIMINATOR_LENGTH) {
return InvalidReason.TOO_LONG
}
return try {
// We only want to check the discriminator, so we pass in a known-valid nickname
Username.fromParts("spiderman", value, MIN_NICKNAME_LENGTH, MAX_NICKNAME_LENGTH)
null
} catch (e: BadDiscriminatorCharacterException) {
InvalidReason.INVALID_CHARACTERS
} catch (e: DiscriminatorCannotBeEmptyException) {
InvalidReason.TOO_SHORT
} catch (e: DiscriminatorCannotBeSingleDigitException) {
InvalidReason.TOO_SHORT
} catch (e: DiscriminatorCannotBeZeroException) {
if (value.length < 2) {
InvalidReason.TOO_SHORT
} else if (value == "00") {
InvalidReason.INVALID_NUMBER_00
} else {
InvalidReason.INVALID_NUMBER_PREFIX_0
}
value.length > MAX_DISCRIMINATOR_LENGTH -> {
InvalidReason.TOO_LONG
}
value.toIntOrNull() == null -> {
InvalidReason.INVALID_CHARACTERS
}
else -> {
null
} catch (e: DiscriminatorCannotHaveLeadingZerosException) {
if (value.length < 2) {
InvalidReason.TOO_SHORT
} else if (value == "00") {
InvalidReason.INVALID_NUMBER_00
} else {
InvalidReason.INVALID_NUMBER_PREFIX_0
}
} catch (e: DiscriminatorTooLargeException) {
InvalidReason.TOO_LONG
} catch (e: BaseUsernameException) {
Log.w(TAG, "Unhandled verification exception!", e)
InvalidReason.INVALID_CHARACTERS
}
}
@@ -91,6 +127,7 @@ object UsernameUtil {
INVALID_CHARACTERS,
STARTS_WITH_NUMBER,
INVALID_NUMBER,
INVALID_NUMBER_PREFIX
INVALID_NUMBER_00,
INVALID_NUMBER_PREFIX_0
}
}

View File

@@ -2320,8 +2320,8 @@
<string name="UsernameEditFragment__invalid_username_enter_a_maximum_of_d_digits">Invalid username, enter a maximum of %1$d digits.</string>
<!-- Displayed when the chosen discriminator is 00 -->
<string name="UsernameEditFragment__this_number_cant_be_00">This number can\'t be 00. Enter a digit between 19</string>
<!-- Displayed when the chosen discriminator starts with 00 -->
<string name="UsernameEditFragment__this_number_cant_start_with_00">This number can\'t start with 00. Enter a digit between 19</string>
<!-- Displayed when the chosen discriminator starts with 0 and has a length > 2 -->
<string name="UsernameEditFragment__this_number_cant_start_with_0">Numbers with more than 2 digits can\'t start with 0</string>
<!-- The body of an alert dialog asking the user to confirm that they want to recover their username -->
<string name="UsernameEditFragment_recovery_dialog_confirmation">Recovering your username will reset your existing QR code and link. Are you sure?</string>
<!-- The body of an alert dialog asking the user to confirm that they want to change their username, even if it resets their link -->

View File

@@ -3,45 +3,81 @@ package org.thoughtcrime.securesms.util
import org.junit.Test
import org.thoughtcrime.securesms.assertIs
import org.thoughtcrime.securesms.assertIsNull
import org.thoughtcrime.securesms.util.UsernameUtil.checkUsername
import org.thoughtcrime.securesms.util.UsernameUtil.checkDiscriminator
import org.thoughtcrime.securesms.util.UsernameUtil.checkNickname
class UsernameUtilTest {
@Test
fun checkUsername_tooShort() {
checkUsername(null) assertIs UsernameUtil.InvalidReason.TOO_SHORT
checkUsername("") assertIs UsernameUtil.InvalidReason.TOO_SHORT
checkUsername("ab") assertIs UsernameUtil.InvalidReason.TOO_SHORT
checkNickname(null) assertIs UsernameUtil.InvalidReason.TOO_SHORT
checkNickname("") assertIs UsernameUtil.InvalidReason.TOO_SHORT
checkNickname("ab") assertIs UsernameUtil.InvalidReason.TOO_SHORT
}
@Test
fun checkUsername_tooLong() {
checkUsername("abcdefghijklmnopqrstuvwxyz1234567") assertIs UsernameUtil.InvalidReason.TOO_LONG
checkNickname("abcdefghijklmnopqrstuvwxyz1234567") assertIs UsernameUtil.InvalidReason.TOO_LONG
}
@Test
fun checkUsername_startsWithNumber() {
checkUsername("0abcdefg") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER
checkUsername("9abcdefg") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER
checkUsername("8675309") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER
checkNickname("0abcdefg") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER
checkNickname("9abcdefg") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER
checkNickname("8675309") assertIs UsernameUtil.InvalidReason.STARTS_WITH_NUMBER
}
@Test
fun checkUsername_invalidCharacters() {
checkUsername("\$abcd") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkUsername(" abcd") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkUsername("ab cde") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkUsername("%%%%%") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkUsername("-----") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkUsername("asĸ_me") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkUsername("+18675309") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkNickname("\$abcd") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkNickname(" abcd") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkNickname("ab cde") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkNickname("%%%%%") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkNickname("-----") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkNickname("asĸ_me") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkNickname("+18675309") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
}
@Test
fun checkUsername_validUsernames() {
checkUsername("abcd").assertIsNull()
checkUsername("abcdefghijklmnopqrstuvwxyz").assertIsNull()
checkUsername("ABCDEFGHIJKLMNOPQRSTUVWXYZ").assertIsNull()
checkUsername("web_head").assertIsNull()
checkUsername("Spider_Fan_1991").assertIsNull()
checkNickname("abcd").assertIsNull()
checkNickname("abcdefghijklmnopqrstuvwxyz").assertIsNull()
checkNickname("ABCDEFGHIJKLMNOPQRSTUVWXYZ").assertIsNull()
checkNickname("web_head").assertIsNull()
checkNickname("Spider_Fan_1991").assertIsNull()
}
@Test
fun checkDiscriminator_valid() {
checkDiscriminator(null).assertIsNull()
checkDiscriminator("01").assertIsNull()
checkDiscriminator("111111111").assertIsNull()
}
@Test
fun checkDiscriminator_tooShort() {
checkDiscriminator("0") assertIs UsernameUtil.InvalidReason.TOO_SHORT
checkDiscriminator("") assertIs UsernameUtil.InvalidReason.TOO_SHORT
}
@Test
fun checkDiscriminator_tooLong() {
checkDiscriminator("1111111111") assertIs UsernameUtil.InvalidReason.TOO_LONG
}
@Test
fun checkDiscriminator_00() {
checkDiscriminator("00") assertIs UsernameUtil.InvalidReason.INVALID_NUMBER_00
}
@Test
fun checkDiscriminator_prefixZero() {
checkDiscriminator("001") assertIs UsernameUtil.InvalidReason.INVALID_NUMBER_PREFIX_0
checkDiscriminator("0001") assertIs UsernameUtil.InvalidReason.INVALID_NUMBER_PREFIX_0
checkDiscriminator("011") assertIs UsernameUtil.InvalidReason.INVALID_NUMBER_PREFIX_0
}
fun checkDiscriminator_invalidChars() {
checkDiscriminator("a1") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
checkDiscriminator("1x") assertIs UsernameUtil.InvalidReason.INVALID_CHARACTERS
}
}