Stored hashed username

This commit is contained in:
Katherine Yen
2023-02-01 12:08:25 -08:00
committed by GitHub
parent 448365c7a0
commit d93d50d038
41 changed files with 799 additions and 1474 deletions

View File

@@ -6,12 +6,12 @@
package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doThrow;
@@ -24,7 +24,6 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.HttpHeaders;
import com.google.i18n.phonenumbers.NumberParseException;
@@ -37,6 +36,7 @@ import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -79,20 +79,20 @@ import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
import org.whispersystems.textsecuregcm.entities.ChangePhoneNumberRequest;
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameRequest;
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.RegistrationLock;
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameRequest;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameResponse;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.entities.UsernameRequest;
import org.whispersystems.textsecuregcm.entities.UsernameResponse;
import org.whispersystems.textsecuregcm.limits.RateLimitByIpFilter;
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.JsonMappingExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberExceptionMapper;
import org.whispersystems.textsecuregcm.mappers.NonNormalizedPhoneNumberResponse;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
@@ -108,7 +108,7 @@ import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
import org.whispersystems.textsecuregcm.storage.UsernameNotAvailableException;
import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException;
import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
@@ -119,7 +119,6 @@ import org.whispersystems.textsecuregcm.util.TestClock;
@ExtendWith(DropwizardExtensionsSupport.class)
class AccountControllerTest {
private static final String SENDER = "+14152222222";
private static final String SENDER_OLD = "+14151111111";
private static final String SENDER_PIN = "+14153333333";
@@ -131,10 +130,18 @@ class AccountControllerTest {
private static final String SENDER_TRANSFER = "+14151111112";
private static final String RESTRICTED_COUNTRY = "800";
private static final String RESTRICTED_NUMBER = "+" + RESTRICTED_COUNTRY + "11111111";
private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc";
private static final String INVALID_BASE_64_URL_USERNAME_HASH = "fA+VkNbvB6dVfx/6NpaRSK6mvhhAUBgDNWFaD7+7gvs=";
private static final String TOO_SHORT_BASE_64_URL_USERNAME_HASH = "P2oMuxx0xgGxSpTO0ACq3IztEOBDaV9t9YFu4bAGpQ";
private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1);
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
private static final byte[] INVALID_USERNAME_HASH = Base64.getDecoder().decode(INVALID_BASE_64_URL_USERNAME_HASH);
private static final byte[] TOO_SHORT_USERNAME_HASH = Base64.getUrlDecoder().decode(TOO_SHORT_BASE_64_URL_USERNAME_HASH);
private static final UUID SENDER_REG_LOCK_UUID = UUID.randomUUID();
private static final UUID SENDER_TRANSFER_UUID = UUID.randomUUID();
private static final UUID RESERVATION_TOKEN = UUID.randomUUID();
private static final String NICE_HOST = "127.0.0.1";
private static final String RATE_LIMITED_IP_HOST = "10.0.0.1";
@@ -186,6 +193,7 @@ class AccountControllerTest {
new PolymorphicAuthValueFactoryProvider.Binder<>(
ImmutableSet.of(AuthenticatedAccount.class,
DisabledPermittedAuthenticatedAccount.class)))
.addProvider(new JsonMappingExceptionMapper())
.addProvider(new RateLimitExceededExceptionMapper())
.addProvider(new ImpossiblePhoneNumberExceptionMapper())
.addProvider(new NonNormalizedPhoneNumberExceptionMapper())
@@ -272,9 +280,6 @@ class AccountControllerTest {
return account;
});
when(accountsManager.setUsername(AuthHelper.VALID_ACCOUNT, "takenusername", null))
.thenThrow(new UsernameNotAvailableException());
when(changeNumberManager.changeNumber(any(), any(), any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> {
final Account account = invocation.getArgument(0, Account.class);
final String number = invocation.getArgument(1, String.class);
@@ -1648,143 +1653,140 @@ class AccountControllerTest {
}
@Test
void testSetUsername() throws UsernameNotAvailableException {
Account account = mock(Account.class);
when(account.getUsername()).thenReturn(Optional.of("N00bkilleR.1234"));
when(accountsManager.setUsername(any(), eq("N00bkilleR"), isNull()))
.thenReturn(account);
void testReserveUsernameHash() throws UsernameHashNotAvailableException {
when(accountsManager.reserveUsernameHash(any(), any()))
.thenReturn(new AccountsManager.UsernameReservation(null, USERNAME_HASH_1));
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(new UsernameRequest("N00bkilleR", null)));
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.readEntity(UsernameResponse.class).username()).isEqualTo("N00bkilleR.1234");
}
@Test
void testReserveUsername() throws UsernameNotAvailableException {
when(accountsManager.reserveUsername(any(), eq("N00bkilleR")))
.thenReturn(new AccountsManager.UsernameReservation(null, "N00bkilleR.1234", RESERVATION_TOKEN));
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username/reserved")
.target("/v1/accounts/username_hash/reserve")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(new ReserveUsernameRequest("N00bkilleR")));
.put(Entity.json(new ReserveUsernameHashRequest(List.of(USERNAME_HASH_1, USERNAME_HASH_2))));
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.readEntity(ReserveUsernameResponse.class))
.satisfies(r -> r.username().equals("N00bkilleR.1234"))
.satisfies(r -> r.reservationToken().equals(RESERVATION_TOKEN));
assertThat(response.readEntity(ReserveUsernameHashResponse.class))
.satisfies(r -> assertThat(r.usernameHash()).hasSize(32));
}
@Test
void testCommitUsername() throws UsernameNotAvailableException, UsernameReservationNotFoundException {
Account account = mock(Account.class);
when(account.getUsername()).thenReturn(Optional.of("n00bkiller.1234"));
when(accountsManager.confirmReservedUsername(any(), eq("n00bkiller.1234"), eq(RESERVATION_TOKEN))).thenReturn(account);
void testReserveUsernameHashUnavailable() throws UsernameHashNotAvailableException {
when(accountsManager.reserveUsernameHash(any(), anyList()))
.thenThrow(new UsernameHashNotAvailableException());
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username/confirm")
.target("/v1/accounts/username_hash/reserve")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(new ConfirmUsernameRequest("n00bkiller.1234", RESERVATION_TOKEN)));
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.readEntity(UsernameResponse.class).username()).isEqualTo("n00bkiller.1234");
.put(Entity.json(new ReserveUsernameHashRequest(List.of(USERNAME_HASH_1, USERNAME_HASH_2))));
assertThat(response.getStatus()).isEqualTo(409);
}
@ParameterizedTest
@MethodSource
void testReserveUsernameHashListSizeInvalid(List<byte[]> usernameHashes) {
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username_hash/reserve")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(new ReserveUsernameHashRequest(usernameHashes)));
assertThat(response.getStatus()).isEqualTo(422);
}
static Stream<Arguments> testReserveUsernameHashListSizeInvalid() {
return Stream.of(
Arguments.of(Collections.nCopies(21, USERNAME_HASH_1)),
Arguments.of(Collections.emptyList())
);
}
@Test
void testCommitUnreservedUsername() throws UsernameNotAvailableException, UsernameReservationNotFoundException {
when(accountsManager.confirmReservedUsername(any(), eq("n00bkiller.1234"), eq(RESERVATION_TOKEN)))
void testReserveUsernameHashInvalidHashSize() {
List<byte[]> usernameHashes = List.of(new byte[31]);
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username_hash/reserve")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(new ReserveUsernameHashRequest(usernameHashes)));
assertThat(response.getStatus()).isEqualTo(422);
}
@Test
void testReserveUsernameHashInvalidBase64UrlEncoding() {
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username_hash/reserve")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(
// Has '+' and '='characters which are invalid in base64url
"""
{
"usernameHashes": ["jh1jJ50oGn9wUXAFNtDus6AJgWOQ6XbZzF+wCv7OOQs="]
}
"""));
assertThat(response.getStatus()).isEqualTo(422);
}
@Test
void testCommitUsername() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
Account account = mock(Account.class);
when(account.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH_1));
when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1))).thenReturn(account);
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username_hash/confirm")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1)));
assertThat(response.getStatus()).isEqualTo(200);
assertArrayEquals(response.readEntity(UsernameHashResponse.class).usernameHash(), USERNAME_HASH_1);
}
@Test
void testCommitUnreservedUsername() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1)))
.thenThrow(new UsernameReservationNotFoundException());
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username/confirm")
.target("/v1/accounts/username_hash/confirm")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(new ConfirmUsernameRequest("n00bkiller.1234", RESERVATION_TOKEN)));
.put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1)));
assertThat(response.getStatus()).isEqualTo(409);
}
@Test
void testCommitLapsedUsername() throws UsernameNotAvailableException, UsernameReservationNotFoundException {
when(accountsManager.confirmReservedUsername(any(), eq("n00bkiller.1234"), eq(RESERVATION_TOKEN)))
.thenThrow(new UsernameNotAvailableException());
void testCommitLapsedUsername() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
when(accountsManager.confirmReservedUsernameHash(any(), eq(USERNAME_HASH_1)))
.thenThrow(new UsernameHashNotAvailableException());
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username/confirm")
.target("/v1/accounts/username_hash/confirm")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(new ConfirmUsernameRequest("n00bkiller.1234", RESERVATION_TOKEN)));
.put(Entity.json(new ConfirmUsernameHashRequest(USERNAME_HASH_1)));
assertThat(response.getStatus()).isEqualTo(410);
}
@Test
void testSetTakenUsername() {
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(new UsernameRequest("takenusername", null)));
assertThat(response.getStatus()).isEqualTo(409);
}
@Test
void testSetInvalidUsername() {
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
// contains non-ascii character
.put(Entity.json(new UsernameRequest("pаypal", null)));
assertThat(response.getStatus()).isEqualTo(422);
}
@Test
void testSetInvalidPrefixUsername() throws JsonProcessingException {
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(new UsernameRequest("0n00bkiller", null)));
assertThat(response.getStatus()).isEqualTo(422);
}
@Test
void testSetUsernameBadAuth() {
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
.put(Entity.json(new UsernameRequest("n00bkiller", null)));
assertThat(response.getStatus()).isEqualTo(401);
}
@Test
void testDeleteUsername() {
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username/")
.target("/v1/accounts/username_hash/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.delete();
assertThat(response.getStatus()).isEqualTo(204);
verify(accountsManager).clearUsername(AuthHelper.VALID_ACCOUNT);
verify(accountsManager).clearUsernameHash(AuthHelper.VALID_ACCOUNT);
}
@Test
void testDeleteUsernameBadAuth() {
Response response =
resources.getJerseyTest()
.target("/v1/accounts/username/")
.target("/v1/accounts/username_hash/")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.INVALID_PASSWORD))
.delete();
@@ -1998,9 +2000,9 @@ class AccountControllerTest {
final UUID uuid = UUID.randomUUID();
when(account.getUuid()).thenReturn(uuid);
when(accountsManager.getByUsername(eq("n00bkiller.1234"))).thenReturn(Optional.of(account));
when(accountsManager.getByUsernameHash(any())).thenReturn(Optional.of(account));
Response response = resources.getJerseyTest()
.target("v1/accounts/username/n00bkiller.1234")
.target(String.format("v1/accounts/username_hash/%s", BASE_64_URL_USERNAME_HASH_1))
.request()
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
.get();
@@ -2010,9 +2012,9 @@ class AccountControllerTest {
@Test
void testLookupUsernameDoesNotExist() {
when(accountsManager.getByUsername(eq("n00bkiller.1234"))).thenReturn(Optional.empty());
when(accountsManager.getByUsernameHash(any())).thenReturn(Optional.empty());
assertThat(resources.getJerseyTest()
.target("v1/accounts/username/n00bkiller.1234")
.target(String.format("v1/accounts/username_hash/%s", BASE_64_URL_USERNAME_HASH_1))
.request()
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
.get().getStatus()).isEqualTo(404);
@@ -2024,7 +2026,7 @@ class AccountControllerTest {
MockUtils.updateRateLimiterResponseToFail(
rateLimiters, RateLimiters.Handle.USERNAME_LOOKUP, "127.0.0.1", expectedRetryAfter);
final Response response = resources.getJerseyTest()
.target("/v1/accounts/username/test.123")
.target(String.format("v1/accounts/username_hash/%s", BASE_64_URL_USERNAME_HASH_1))
.request()
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
.get();
@@ -2033,6 +2035,34 @@ class AccountControllerTest {
assertThat(response.getHeaderString("Retry-After")).isEqualTo(String.valueOf(expectedRetryAfter.toSeconds()));
}
@Test
void testLookupUsernameAuthenticated() {
assertThat(resources.getJerseyTest()
.target(String.format("/v1/accounts/username_hash/%s", USERNAME_HASH_1))
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
.get()
.getStatus()).isEqualTo(400);
}
@Test
void testLookupUsernameInvalidFormat() {
assertThat(resources.getJerseyTest()
.target(String.format("/v1/accounts/username_hash/%s", INVALID_USERNAME_HASH))
.request()
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
.get()
.getStatus()).isEqualTo(422);
assertThat(resources.getJerseyTest()
.target(String.format("/v1/accounts/username_hash/%s", TOO_SHORT_USERNAME_HASH))
.request()
.header(HttpHeaders.X_FORWARDED_FOR, "127.0.0.1")
.get()
.getStatus()).isEqualTo(422);
}
@ParameterizedTest
@MethodSource
void pushTokensMatch(@Nullable final String pushChallenge, @Nullable final StoredVerificationCode storedVerificationCode, final boolean expectMatch) {

View File

@@ -31,7 +31,6 @@ import java.security.SecureRandom;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
@@ -132,6 +131,8 @@ class ProfileControllerTest {
private static final String ACCOUNT_PHONE_NUMBER_IDENTITY_KEY = "bazz";
private static final String ACCOUNT_TWO_IDENTITY_KEY = "bar";
private static final String ACCOUNT_TWO_PHONE_NUMBER_IDENTITY_KEY = "baz";
private static final String BASE_64_URL_USERNAME_HASH = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
private static final byte[] USERNAME_HASH = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH);
@SuppressWarnings("unchecked")
private static final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = mock(
DynamicConfigurationManager.class);
@@ -197,7 +198,7 @@ class ProfileControllerTest {
when(profileAccount.isAnnouncementGroupSupported()).thenReturn(false);
when(profileAccount.isChangeNumberSupported()).thenReturn(false);
when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.empty());
when(profileAccount.getUsername()).thenReturn(Optional.of("n00bkiller"));
when(profileAccount.getUsernameHash()).thenReturn(Optional.of(USERNAME_HASH));
when(profileAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes()));
Account capabilitiesAccount = mock(Account.class);
@@ -212,7 +213,7 @@ class ProfileControllerTest {
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount));
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID_TWO)).thenReturn(Optional.of(profileAccount));
when(accountsManager.getByPhoneNumberIdentifier(AuthHelper.VALID_PNI_TWO)).thenReturn(Optional.of(profileAccount));
when(accountsManager.getByUsername("n00bkiller")).thenReturn(Optional.of(profileAccount));
when(accountsManager.getByUsernameHash(USERNAME_HASH)).thenReturn(Optional.of(profileAccount));
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of(capabilitiesAccount));
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(capabilitiesAccount));

View File

@@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Base64;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
@@ -26,8 +27,10 @@ class AccountChangeValidatorTest {
private static final UUID ORIGINAL_PNI = UUID.randomUUID();
private static final UUID CHANGED_PNI = UUID.randomUUID();
private static final String ORIGINAL_USERNAME = "bruce_wayne";
private static final String CHANGED_USERNAME = "batman";
private static final String BASE_64_URL_ORIGINAL_USERNAME = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
private static final String BASE_64_URL_CHANGED_USERNAME = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc";
private static final byte[] ORIGINAL_USERNAME_HASH = Base64.getUrlDecoder().decode(BASE_64_URL_ORIGINAL_USERNAME);
private static final byte[] CHANGED_USERNAME_HASH = Base64.getUrlDecoder().decode(BASE_64_URL_CHANGED_USERNAME);
@ParameterizedTest
@MethodSource
@@ -49,22 +52,22 @@ class AccountChangeValidatorTest {
final Account originalAccount = mock(Account.class);
when(originalAccount.getNumber()).thenReturn(ORIGINAL_NUMBER);
when(originalAccount.getPhoneNumberIdentifier()).thenReturn(ORIGINAL_PNI);
when(originalAccount.getUsername()).thenReturn(Optional.of(ORIGINAL_USERNAME));
when(originalAccount.getUsernameHash()).thenReturn(Optional.of(ORIGINAL_USERNAME_HASH));
final Account unchangedAccount = mock(Account.class);
when(unchangedAccount.getNumber()).thenReturn(ORIGINAL_NUMBER);
when(unchangedAccount.getPhoneNumberIdentifier()).thenReturn(ORIGINAL_PNI);
when(unchangedAccount.getUsername()).thenReturn(Optional.of(ORIGINAL_USERNAME));
when(unchangedAccount.getUsernameHash()).thenReturn(Optional.of(ORIGINAL_USERNAME_HASH));
final Account changedNumberAccount = mock(Account.class);
when(changedNumberAccount.getNumber()).thenReturn(CHANGED_NUMBER);
when(changedNumberAccount.getPhoneNumberIdentifier()).thenReturn(CHANGED_PNI);
when(changedNumberAccount.getUsername()).thenReturn(Optional.of(ORIGINAL_USERNAME));
when(changedNumberAccount.getUsernameHash()).thenReturn(Optional.of(ORIGINAL_USERNAME_HASH));
final Account changedUsernameAccount = mock(Account.class);
when(changedUsernameAccount.getNumber()).thenReturn(ORIGINAL_NUMBER);
when(changedUsernameAccount.getPhoneNumberIdentifier()).thenReturn(ORIGINAL_PNI);
when(changedUsernameAccount.getUsername()).thenReturn(Optional.of(CHANGED_USERNAME));
when(changedUsernameAccount.getUsernameHash()).thenReturn(Optional.of(CHANGED_USERNAME_HASH));
return Stream.of(
Arguments.of(originalAccount, unchangedAccount, AccountChangeValidator.GENERAL_CHANGE_VALIDATOR, true),

View File

@@ -33,7 +33,6 @@ import org.whispersystems.textsecuregcm.redis.RedisClusterExtension;
import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex;
@@ -192,13 +191,11 @@ class AccountsManagerChangeNumberIntegrationTest {
mock(DirectoryQueue.class),
mock(Keys.class),
mock(MessagesManager.class),
mock(ProhibitedUsernames.class),
mock(ProfilesManager.class),
mock(StoredVerificationCodeManager.class),
secureStorageClient,
secureBackupClient,
clientPresenceManager,
mock(UsernameGenerator.class),
mock(ExperimentEnrollmentManager.class),
mock(Clock.class));
}

View File

@@ -51,7 +51,6 @@ import org.whispersystems.textsecuregcm.tests.util.DevicesHelper;
import org.whispersystems.textsecuregcm.tests.util.JsonHelpers;
import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement;
@@ -159,13 +158,11 @@ class AccountsManagerConcurrentModificationIntegrationTest {
mock(DirectoryQueue.class),
mock(Keys.class),
mock(MessagesManager.class),
mock(ProhibitedUsernames.class),
mock(ProfilesManager.class),
mock(StoredVerificationCodeManager.class),
mock(SecureStorageClient.class),
mock(SecureBackupClient.class),
mock(ClientPresenceManager.class),
mock(UsernameGenerator.class),
mock(ExperimentEnrollmentManager.class),
mock(Clock.class)
);

View File

@@ -5,18 +5,15 @@
package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.AdditionalMatchers.and;
import static org.mockito.AdditionalMatchers.not;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
@@ -31,15 +28,17 @@ import static org.mockito.Mockito.when;
import io.lettuce.core.RedisException;
import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands;
import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -47,9 +46,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentMatcher;
import org.mockito.stubbing.Answer;
import org.whispersystems.textsecuregcm.configuration.UsernameConfiguration;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
@@ -62,10 +59,12 @@ import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
import org.whispersystems.textsecuregcm.tests.util.RedisClusterHelper;
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
import org.whispersystems.textsecuregcm.util.UsernameNormalizer;
class AccountsManagerTest {
private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc";
private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1);
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
private Accounts accounts;
private DeletedAccountsManager deletedAccountsManager;
@@ -73,7 +72,6 @@ class AccountsManagerTest {
private Keys keys;
private MessagesManager messagesManager;
private ProfilesManager profilesManager;
private ProhibitedUsernames prohibitedUsernames;
private ExperimentEnrollmentManager enrollmentManager;
private Map<String, UUID> phoneNumberIdentifiersByE164;
@@ -89,8 +87,6 @@ class AccountsManagerTest {
return null;
};
private static final UUID RESERVATION_TOKEN = UUID.randomUUID();
@BeforeEach
void setup() throws InterruptedException {
accounts = mock(Accounts.class);
@@ -99,7 +95,6 @@ class AccountsManagerTest {
keys = mock(Keys.class);
messagesManager = mock(MessagesManager.class);
profilesManager = mock(ProfilesManager.class);
prohibitedUsernames = mock(ProhibitedUsernames.class);
//noinspection unchecked
commands = mock(RedisAdvancedClusterCommands.class);
@@ -143,7 +138,7 @@ class AccountsManagerTest {
enrollmentManager = mock(ExperimentEnrollmentManager.class);
when(enrollmentManager.isEnrolled(any(UUID.class), eq(AccountsManager.USERNAME_EXPERIMENT_NAME))).thenReturn(true);
when(accounts.usernameAvailable(any())).thenReturn(true);
when(accounts.usernameHashAvailable(any())).thenReturn(true);
accountsManager = new AccountsManager(
accounts,
@@ -153,13 +148,11 @@ class AccountsManagerTest {
directoryQueue,
keys,
messagesManager,
prohibitedUsernames,
profilesManager,
mock(StoredVerificationCodeManager.class),
storageClient,
backupClient,
mock(ClientPresenceManager.class),
new UsernameGenerator(new UsernameConfiguration()),
enrollmentManager,
mock(Clock.class));
}
@@ -169,7 +162,8 @@ class AccountsManagerTest {
UUID uuid = UUID.randomUUID();
when(commands.get(eq("AccountMap::+14152222222"))).thenReturn(uuid.toString());
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
when(commands.get(eq("Account3::" + uuid))).thenReturn(
"{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
Optional<Account> account = accountsManager.getByE164("+14152222222");
@@ -188,7 +182,8 @@ class AccountsManagerTest {
void testGetAccountByUuidInCache() {
UUID uuid = UUID.randomUUID();
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
when(commands.get(eq("Account3::" + uuid))).thenReturn(
"{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
Optional<Account> account = accountsManager.getByAccountIdentifier(uuid);
@@ -209,7 +204,8 @@ class AccountsManagerTest {
UUID pni = UUID.randomUUID();
when(commands.get(eq("AccountMap::" + pni))).thenReturn(uuid.toString());
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
when(commands.get(eq("Account3::" + uuid))).thenReturn(
"{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}");
Optional<Account> account = accountsManager.getByPhoneNumberIdentifier(pni);
@@ -225,21 +221,21 @@ class AccountsManagerTest {
}
@Test
void testGetByUsernameInCache() {
void testGetByUsernameHashInCache() {
UUID uuid = UUID.randomUUID();
String username = "test";
when(commands.get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1))).thenReturn(uuid.toString());
when(commands.get(eq("Account3::" + uuid))).thenReturn(
String.format("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\", \"usernameHash\": \"%s\"}",
BASE_64_URL_USERNAME_HASH_1));
when(commands.get(eq("UAccountMap::" + username))).thenReturn(uuid.toString());
when(commands.get(eq("Account3::" + uuid))).thenReturn("{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\", \"username\": \"test\"}");
Optional<Account> account = accountsManager.getByUsername(username);
Optional<Account> account = accountsManager.getByUsernameHash(USERNAME_HASH_1);
assertTrue(account.isPresent());
assertEquals(account.get().getNumber(), "+14152222222");
assertEquals(UUID.fromString("de24dc73-fbd8-41be-a7d5-764c70d9da7e"), account.get().getPhoneNumberIdentifier());
assertEquals(Optional.of(username), account.get().getUsername());
assertArrayEquals(USERNAME_HASH_1, account.get().getUsernameHash().get());
verify(commands).get(eq("UAccountMap::" + username));
verify(commands).get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1));
verify(commands).get(eq("Account3::" + uuid));
verifyNoMoreInteractions(commands);
@@ -320,29 +316,28 @@ class AccountsManagerTest {
}
@Test
void testGetAccountByUsernameNotInCache() {
void testGetAccountByUsernameHashNotInCache() {
UUID uuid = UUID.randomUUID();
String username = "test";
Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]);
account.setUsername(username);
account.setUsernameHash(USERNAME_HASH_1);
when(commands.get(eq("UAccountMap::" + username))).thenReturn(null);
when(accounts.getByUsername(username)).thenReturn(Optional.of(account));
when(commands.get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1))).thenReturn(null);
when(accounts.getByUsernameHash(USERNAME_HASH_1)).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.getByUsername(username);
Optional<Account> retrieved = accountsManager.getByUsernameHash(USERNAME_HASH_1);
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), account);
verify(commands).get(eq("UAccountMap::" + username));
verify(commands).setex(eq("UAccountMap::" + username), anyLong(), eq(uuid.toString()));
verify(commands).get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1));
verify(commands).setex(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("AccountMap::" + account.getPhoneNumberIdentifier()), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("Account3::" + uuid), anyLong(), anyString());
verifyNoMoreInteractions(commands);
verify(accounts).getByUsername(username);
verify(accounts).getByUsernameHash(USERNAME_HASH_1);
verifyNoMoreInteractions(accounts);
}
@@ -422,27 +417,26 @@ class AccountsManagerTest {
@Test
void testGetAccountByUsernameBrokenCache() {
UUID uuid = UUID.randomUUID();
String username = "test";
Account account = AccountsHelper.generateTestAccount("+14152222222", uuid, UUID.randomUUID(), new ArrayList<>(), new byte[16]);
account.setUsername(username);
account.setUsernameHash(USERNAME_HASH_1);
when(commands.get(eq("UAccountMap::" + username))).thenThrow(new RedisException("OH NO"));
when(accounts.getByUsername(username)).thenReturn(Optional.of(account));
when(commands.get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1))).thenThrow(new RedisException("OH NO"));
when(accounts.getByUsernameHash(USERNAME_HASH_1)).thenReturn(Optional.of(account));
Optional<Account> retrieved = accountsManager.getByUsername(username);
Optional<Account> retrieved = accountsManager.getByUsernameHash(USERNAME_HASH_1);
assertTrue(retrieved.isPresent());
assertSame(retrieved.get(), account);
verify(commands).get(eq("UAccountMap::" + username));
verify(commands).setex(eq("UAccountMap::" + username), anyLong(), eq(uuid.toString()));
verify(commands).get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1));
verify(commands).setex(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("AccountMap::" + account.getPhoneNumberIdentifier()), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("AccountMap::+14152222222"), anyLong(), eq(uuid.toString()));
verify(commands).setex(eq("Account3::" + uuid), anyLong(), anyString());
verifyNoMoreInteractions(commands);
verify(accounts).getByUsername(username);
verify(accounts).getByUsernameHash(USERNAME_HASH_1);
verifyNoMoreInteractions(accounts);
}
@@ -734,188 +728,96 @@ class AccountsManagerTest {
}
@Test
void testSetUsername() {
void testReserveUsernameHash() throws UsernameHashNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String nickname = "test";
assertDoesNotThrow(() -> accountsManager.setUsername(account, nickname, null));
verify(accounts).setUsername(eq(account), startsWith(nickname));
final List<byte[]> usernameHashes = List.of(new byte[32], new byte[32]);
when(accounts.usernameHashAvailable(any())).thenReturn(true);
accountsManager.reserveUsernameHash(account, usernameHashes);
verify(accounts).reserveUsernameHash(eq(account), eq(new byte[32]), eq(Duration.ofMinutes(5)));
}
@Test
void testReserveUsername() throws UsernameNotAvailableException {
void testReserveUsernameHashNotAvailable() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String nickname = "beethoven";
accountsManager.reserveUsername(account, nickname);
verify(accounts).reserveUsername(eq(account), startsWith(nickname), any());
when(accounts.usernameHashAvailable(any())).thenReturn(false);
assertThrows(UsernameHashNotAvailableException.class, () -> accountsManager.reserveUsernameHash(account, List.of(
USERNAME_HASH_1, USERNAME_HASH_2)));
}
@Test
void testSetReservedUsername() throws UsernameNotAvailableException, UsernameReservationNotFoundException {
void testReserveUsernameDisabled() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String reserved = "sCoObY.1234";
setReservationHash(account, reserved);
when(accounts.usernameAvailable(eq(Optional.of(RESERVATION_TOKEN)), eq(reserved))).thenReturn(true);
accountsManager.confirmReservedUsername(account, reserved, RESERVATION_TOKEN);
verify(accounts).confirmUsername(eq(account), eq(reserved), eq(RESERVATION_TOKEN));
when(enrollmentManager.isEnrolled(account.getUuid(), AccountsManager.USERNAME_EXPERIMENT_NAME)).thenReturn(false);
assertThrows(UsernameHashNotAvailableException.class, () -> accountsManager.reserveUsernameHash(account, List.of(
USERNAME_HASH_1)));
}
@Test
void testSetReservedHashNameMismatch() {
void testConfirmReservedUsernameHash() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
setReservationHash(account, "pluto.1234");
when(accounts.usernameAvailable(eq(Optional.of(RESERVATION_TOKEN)), eq("pluto.1234"))).thenReturn(true);
setReservationHash(account, USERNAME_HASH_1);
when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1))).thenReturn(true);
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1);
verify(accounts).confirmUsernameHash(eq(account), eq(USERNAME_HASH_1));
}
@Test
void testConfirmReservedHashNameMismatch() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
setReservationHash(account, USERNAME_HASH_1);
when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1))).thenReturn(true);
assertThrows(UsernameReservationNotFoundException.class,
() -> accountsManager.confirmReservedUsername(account, "goofy.1234", RESERVATION_TOKEN));
() -> accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_2));
}
@Test
void testSetReservedHashAciMismatch() {
void testConfirmReservedLapsed() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String reserved = "toto.1234";
account.setReservedUsernameHash(Accounts.reservedUsernameHash(UUID.randomUUID(), reserved));
when(accounts.usernameAvailable(eq(Optional.of(RESERVATION_TOKEN)), eq(reserved))).thenReturn(true);
assertThrows(UsernameReservationNotFoundException.class,
() -> accountsManager.confirmReservedUsername(account, reserved, RESERVATION_TOKEN));
// hash was reserved, but the reservation lapsed and another account took it
setReservationHash(account, USERNAME_HASH_1);
when(accounts.usernameHashAvailable(eq(Optional.of(account.getUuid())), eq(USERNAME_HASH_1))).thenReturn(false);
assertThrows(UsernameHashNotAvailableException.class, () -> accountsManager.confirmReservedUsernameHash(account,
USERNAME_HASH_1));
verify(accounts, never()).confirmUsernameHash(any(), any());
}
@Test
void testSetReservedLapsed() {
void testConfirmReservedRetry() throws UsernameHashNotAvailableException, UsernameReservationNotFoundException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String reserved = "porkchop.1234";
// name was reserved, but the reservation lapsed and another account took it
setReservationHash(account, reserved);
when(accounts.usernameAvailable(eq(Optional.of(RESERVATION_TOKEN)), eq(reserved))).thenReturn(false);
assertThrows(UsernameNotAvailableException.class, () -> accountsManager.confirmReservedUsername(account, reserved, RESERVATION_TOKEN));
verify(accounts, never()).confirmUsername(any(), any(), any());
}
@Test
void testSetReservedRetry() throws UsernameNotAvailableException, UsernameReservationNotFoundException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String username = "santaslittlehelper.1234";
account.setUsername(username);
account.setUsernameHash(USERNAME_HASH_1);
// reserved username already set, should be treated as a replay
accountsManager.confirmReservedUsername(account, username, RESERVATION_TOKEN);
accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1);
verifyNoInteractions(accounts);
}
@Test
void testSetUsernameSameUsername() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String nickname = "test";
account.setUsername(nickname + ".123");
// should be treated as a replayed request
assertDoesNotThrow(() -> accountsManager.setUsername(account, nickname, null));
verify(accounts, never()).setUsername(eq(account), any());
}
@Test
void testSetUsernameReroll() throws UsernameNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String nickname = "test";
final String username = nickname + ".ZZZ";
account.setUsername(username);
// given the correct old username, should reroll discriminator even if the nick matches
accountsManager.setUsername(account, nickname, username);
verify(accounts).setUsername(eq(account), and(startsWith(nickname), not(eq(username))));
}
@Test
void testReserveUsernameReroll() throws UsernameNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String nickname = "clifford";
final String username = nickname + ".ZZZ";
account.setUsername(username);
// given the correct old username, should reroll discriminator even if the nick matches
accountsManager.reserveUsername(account, nickname);
verify(accounts).reserveUsername(eq(account), and(startsWith(nickname), not(eq(username))), any());
}
@Test
void testSetReservedUsernameWithNoReservation() {
void testConfirmReservedUsernameHashWithNoReservation() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(),
new ArrayList<>(), new byte[16]);
assertThrows(UsernameReservationNotFoundException.class,
() -> accountsManager.confirmReservedUsername(account, "laika.1234", RESERVATION_TOKEN));
verify(accounts, never()).confirmUsername(any(), any(), any());
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
void testUsernameExpandDiscriminator(boolean reserve) throws UsernameNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String nickname = "test";
ArgumentMatcher<String> isWide = (String username) -> {
String[] spl = username.split(Pattern.quote(UsernameGenerator.SEPARATOR));
assertEquals(spl.length, 2);
int discriminator = Integer.parseInt(spl[1]);
// require a 7 digit discriminator
return discriminator > 1_000_000;
};
when(accounts.usernameAvailable(any())).thenReturn(false);
when(accounts.usernameAvailable(argThat(isWide))).thenReturn(true);
if (reserve) {
accountsManager.reserveUsername(account, nickname);
verify(accounts).reserveUsername(eq(account), and(startsWith(nickname), argThat(isWide)), any());
} else {
accountsManager.setUsername(account, nickname, null);
verify(accounts).setUsername(eq(account), and(startsWith(nickname), argThat(isWide)));
}
() -> accountsManager.confirmReservedUsernameHash(account, USERNAME_HASH_1));
verify(accounts, never()).confirmUsernameHash(any(), any());
}
@Test
void testChangeUsername() throws UsernameNotAvailableException {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String nickname = "test";
account.setUsername("old.123");
accountsManager.setUsername(account, nickname, "old.123");
verify(accounts).setUsername(eq(account), startsWith(nickname));
}
@Test
void testSetUsernameNotAvailable() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
final String nickname = "unavailable";
when(accounts.usernameAvailable(startsWith(nickname))).thenReturn(false);
assertThrows(UsernameNotAvailableException.class, () -> accountsManager.setUsername(account, nickname, null));
verify(accounts, never()).setUsername(any(), any());
assertTrue(account.getUsername().isEmpty());
}
@Test
void testSetUsernameReserved() {
final String nickname = "reserved";
when(prohibitedUsernames.isProhibited(eq(nickname), any())).thenReturn(true);
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
assertThrows(UsernameNotAvailableException.class, () -> accountsManager.setUsername(account, nickname, null));
assertTrue(account.getUsername().isEmpty());
void testClearUsernameHash() {
Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
account.setUsernameHash(USERNAME_HASH_1);
accountsManager.clearUsernameHash(account);
verify(accounts).clearUsernameHash(eq(account));
}
@Test
void testSetUsernameViaUpdate() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setUsername("test")));
assertThrows(AssertionError.class, () -> accountsManager.update(account, a -> a.setUsernameHash(USERNAME_HASH_1)));
}
@Test
void testSetUsernameDisabled() {
final Account account = AccountsHelper.generateTestAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID(), new ArrayList<>(), new byte[16]);
when(enrollmentManager.isEnrolled(account.getUuid(), AccountsManager.USERNAME_EXPERIMENT_NAME)).thenReturn(false);
assertThrows(UsernameNotAvailableException.class, () -> accountsManager.setUsername(account, "n00bkiller", null));
}
private void setReservationHash(final Account account, final String reservedUsername) {
account.setReservedUsernameHash(Accounts.reservedUsernameHash(account.getUuid(), reservedUsername));
private void setReservationHash(final Account account, final byte[] reservedUsernameHash) {
account.setReservedUsernameHash(reservedUsernameHash);
}
private static Device generateTestDevice(final long lastSeen) {

View File

@@ -6,9 +6,10 @@
package org.whispersystems.textsecuregcm.storage;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
@@ -17,11 +18,15 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@@ -29,8 +34,6 @@ import java.util.function.Consumer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
@@ -42,7 +45,6 @@ import org.whispersystems.textsecuregcm.securebackup.SecureBackupClient;
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
import org.whispersystems.textsecuregcm.sqs.DirectoryQueue;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest;
@@ -59,7 +61,11 @@ class AccountsManagerUsernameIntegrationTest {
private static final String PNI_ASSIGNMENT_TABLE_NAME = "pni_assignment_test";
private static final String USERNAMES_TABLE_NAME = "usernames_test";
private static final String PNI_TABLE_NAME = "pni_test";
private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc";
private static final int SCAN_PAGE_SIZE = 1;
private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1);
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
@RegisterExtension
static DynamoDbExtension ACCOUNTS_DYNAMO_EXTENSION = DynamoDbExtension.builder()
@@ -86,7 +92,6 @@ class AccountsManagerUsernameIntegrationTest {
private AccountsManager accountsManager;
private Accounts accounts;
private UsernameGenerator usernameGenerator;
@BeforeEach
void setup() throws InterruptedException {
@@ -107,12 +112,12 @@ class AccountsManagerUsernameIntegrationTest {
CreateTableRequest createUsernamesTableRequest = CreateTableRequest.builder()
.tableName(USERNAMES_TABLE_NAME)
.keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_USERNAME)
.attributeName(Accounts.ATTR_USERNAME_HASH)
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(Accounts.ATTR_USERNAME)
.attributeType(ScalarAttributeType.S)
.attributeName(Accounts.ATTR_USERNAME_HASH)
.attributeType(ScalarAttributeType.B)
.build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
.build();
@@ -152,8 +157,6 @@ class AccountsManagerUsernameIntegrationTest {
USERNAMES_TABLE_NAME,
SCAN_PAGE_SIZE));
usernameGenerator = new UsernameGenerator(initialWidth, discriminatorMaxWidth, attemptsPerWidth,
Duration.ofDays(1));
final DeletedAccountsManager deletedAccountsManager = mock(DeletedAccountsManager.class);
doAnswer((final InvocationOnMock invocationOnMock) -> {
@SuppressWarnings("unchecked")
@@ -176,211 +179,159 @@ class AccountsManagerUsernameIntegrationTest {
mock(DirectoryQueue.class),
mock(Keys.class),
mock(MessagesManager.class),
mock(ProhibitedUsernames.class),
mock(ProfilesManager.class),
mock(StoredVerificationCodeManager.class),
mock(SecureStorageClient.class),
mock(SecureBackupClient.class),
mock(ClientPresenceManager.class),
usernameGenerator,
experimentEnrollmentManager,
mock(Clock.class));
}
private static int discriminator(String username) {
return Integer.parseInt(username.substring(username.indexOf(UsernameGenerator.SEPARATOR) + 1));
}
@Test
void testSetClearUsername() throws UsernameNotAvailableException, InterruptedException {
void testNoUsernames() throws InterruptedException {
Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
new ArrayList<>());
account = accountsManager.setUsername(account, "n00bkiller", null);
assertThat(account.getUsername()).isPresent();
assertThat(account.getUsername().get()).startsWith("n00bkiller");
int discriminator = discriminator(account.getUsername().get());
assertThat(discriminator).isGreaterThan(0).isLessThan(10);
assertThat(accountsManager.getByUsername(account.getUsername().get()).orElseThrow().getUuid()).isEqualTo(
account.getUuid());
// reroll
account = accountsManager.setUsername(account, "n00bkiller", account.getUsername().get());
final String newUsername = account.getUsername().orElseThrow();
assertThat(discriminator(account.getUsername().orElseThrow())).isNotEqualTo(discriminator);
// clear
account = accountsManager.clearUsername(account);
assertThat(accountsManager.getByUsername(newUsername)).isEmpty();
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsername()).isEmpty();
}
@ParameterizedTest
@ValueSource(booleans = {false, true})
void testNoUsernames(boolean reserve) throws InterruptedException {
Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
new ArrayList<>());
for (int i = 1; i <= 99; i++) {
List<byte[]> usernameHashes = List.of(USERNAME_HASH_1, USERNAME_HASH_2);
int i = 0;
for (byte[] hash : usernameHashes) {
final Map<String, AttributeValue> item = new HashMap<>(Map.of(
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(UUID.randomUUID()),
Accounts.ATTR_USERNAME, AttributeValues.fromString(usernameGenerator.fromParts("n00bkiller", i))));
Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(hash)));
// half of these are taken usernames, half are only reservations (have a TTL)
if (i % 2 == 0) {
item.put(Accounts.ATTR_TTL,
AttributeValues.fromLong(Instant.now().plus(Duration.ofMinutes(1)).getEpochSecond()));
}
i++;
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().putItem(PutItemRequest.builder()
.tableName(USERNAMES_TABLE_NAME)
.item(item)
.build());
}
assertThrows(UsernameNotAvailableException.class, () -> {
if (reserve) {
accountsManager.reserveUsername(account, "n00bkiller");
} else {
accountsManager.setUsername(account, "n00bkiller", null);
}
});
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsername()).isEmpty();
assertThrows(UsernameHashNotAvailableException.class, () -> {accountsManager.reserveUsernameHash(account, usernameHashes);});
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsernameHash()).isEmpty();
}
@Test
void testUsernameSnatched() throws InterruptedException, UsernameNotAvailableException {
void testReserveUsernameSnatched() throws InterruptedException, UsernameHashNotAvailableException {
final Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
new ArrayList<>());
for (int i = 1; i <= 9; i++) {
ArrayList<byte[]> usernameHashes = new ArrayList<>(Arrays.asList(USERNAME_HASH_1, USERNAME_HASH_2));
for (byte[] hash : usernameHashes) {
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().putItem(PutItemRequest.builder()
.tableName(USERNAMES_TABLE_NAME)
.item(Map.of(
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(UUID.randomUUID()),
Accounts.ATTR_USERNAME, AttributeValues.fromString(usernameGenerator.fromParts("n00bkiller", i))))
Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(hash)))
.build());
}
byte[] availableHash = new byte[32];
new SecureRandom().nextBytes(availableHash);
usernameHashes.add(availableHash);
// first time this is called lie and say the username is available
// this simulates seeing an available username and then it being taken
// by someone before the write
doReturn(true).doCallRealMethod().when(accounts).usernameAvailable(any());
final String username = accountsManager
.setUsername(account, "n00bkiller", null)
.getUsername().orElseThrow();
assertThat(username).startsWith("n00bkiller");
assertThat(discriminator(username)).isGreaterThanOrEqualTo(10).isLessThan(100);
doReturn(true).doCallRealMethod().when(accounts).usernameHashAvailable(any());
final byte[] username = accountsManager
.reserveUsernameHash(account, usernameHashes)
.reservedUsernameHash();
assertArrayEquals(username, availableHash);
// 1 attempt on first try (returns true),
// 10 (attempts per width) on width=2 discriminators (all taken)
verify(accounts, times(11)).usernameAvailable(argThat(un -> discriminator(un) < 10));
// 1 final attempt on width=3 discriminators
verify(accounts, times(1)).usernameAvailable(argThat(un -> discriminator(un) >= 10));
// 5 more attempts until "availableHash" returns true
verify(accounts, times(4)).usernameHashAvailable(any());
}
@Test
public void testReserveSetClear()
throws InterruptedException, UsernameNotAvailableException, UsernameReservationNotFoundException {
public void testReserveConfirmClear()
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
new ArrayList<>());
AccountsManager.UsernameReservation reservation = accountsManager.reserveUsername(account, "n00bkiller");
account = reservation.account();
assertThat(account.getReservedUsernameHash()).isPresent();
assertThat(reservation.reservedUsername()).startsWith("n00bkiller");
int discriminator = discriminator(reservation.reservedUsername());
assertThat(discriminator).isGreaterThan(0).isLessThan(10);
assertThat(accountsManager.getByUsername(reservation.reservedUsername())).isEmpty();
account = accountsManager.confirmReservedUsername(
account,
reservation.reservedUsername(),
reservation.reservationToken());
// reserve
AccountsManager.UsernameReservation reservation = accountsManager.reserveUsernameHash(account, List.of(
USERNAME_HASH_1));
assertArrayEquals(reservation.account().getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
assertThat(accountsManager.getByUsernameHash(reservation.reservedUsernameHash())).isEmpty();
assertThat(account.getUsername().get()).startsWith("n00bkiller");
assertThat(accountsManager.getByUsername(account.getUsername().get()).orElseThrow().getUuid()).isEqualTo(
// confirm
account = accountsManager.confirmReservedUsernameHash(
reservation.account(),
reservation.reservedUsernameHash());
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1).orElseThrow().getUuid()).isEqualTo(
account.getUuid());
// reroll
reservation = accountsManager.reserveUsername(account, "n00bkiller");
account = reservation.account();
account = accountsManager.confirmReservedUsername(
account,
reservation.reservedUsername(),
reservation.reservationToken());
final String newUsername = account.getUsername().orElseThrow();
assertThat(discriminator(account.getUsername().orElseThrow())).isNotEqualTo(discriminator);
// clear
account = accountsManager.clearUsername(account);
assertThat(accountsManager.getByUsername(newUsername)).isEmpty();
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsername()).isEmpty();
account = accountsManager.clearUsernameHash(account);
assertThat(accountsManager.getByUsernameHash(USERNAME_HASH_1)).isEmpty();
assertThat(accountsManager.getByAccountIdentifier(account.getUuid()).orElseThrow().getUsernameHash()).isEmpty();
}
@Test
public void testReservationLapsed()
throws InterruptedException, UsernameNotAvailableException, UsernameReservationNotFoundException {
// use a username generator that can retry a lot
buildAccountsManager(1, 1, 1000000);
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
final Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
new ArrayList<>());
AccountsManager.UsernameReservation reservation1 = accountsManager.reserveUsername(account, "n00bkiller");
final String reservedUsername = reservation1.reservedUsername();
AccountsManager.UsernameReservation reservation1 = accountsManager.reserveUsernameHash(account, List.of(
USERNAME_HASH_1));
long past = Instant.now().minus(Duration.ofMinutes(1)).getEpochSecond();
// force expiration
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().updateItem(UpdateItemRequest.builder()
.tableName(USERNAMES_TABLE_NAME)
.key(Map.of(Accounts.ATTR_USERNAME, AttributeValues.fromString(reservedUsername)))
.key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1)))
.updateExpression("SET #ttl = :ttl")
.expressionAttributeNames(Map.of("#ttl", Accounts.ATTR_TTL))
.expressionAttributeValues(Map.of(":ttl", AttributeValues.fromLong(past)))
.build());
int discriminator = discriminator(reservedUsername);
// use up all names except the reserved one
for (int i = 1; i <= 9; i++) {
if (i == discriminator) {
continue;
}
ACCOUNTS_DYNAMO_EXTENSION.getDynamoDbClient().putItem(PutItemRequest.builder()
.tableName(USERNAMES_TABLE_NAME)
.item(Map.of(
Accounts.KEY_ACCOUNT_UUID, AttributeValues.fromUUID(UUID.randomUUID()),
Accounts.ATTR_USERNAME, AttributeValues.fromString(usernameGenerator.fromParts("n00bkiller", i))))
.build());
}
// a different account should be able to reserve it
Account account2 = accountsManager.create("+18005552222", "password", null, new AccountAttributes(),
new ArrayList<>());
final AccountsManager.UsernameReservation reservation2 = accountsManager.reserveUsername(account2, "n00bkiller"
);
assertThat(reservation2.reservedUsername()).isEqualTo(reservedUsername);
final AccountsManager.UsernameReservation reservation2 = accountsManager.reserveUsernameHash(account2, List.of(
USERNAME_HASH_1));
assertArrayEquals(reservation2.reservedUsernameHash(), USERNAME_HASH_1);
assertThrows(UsernameNotAvailableException.class,
() -> accountsManager.confirmReservedUsername(reservation1.account(), reservedUsername, reservation1.reservationToken()));
accountsManager.confirmReservedUsername(reservation2.account(), reservedUsername, reservation2.reservationToken());
assertThrows(UsernameHashNotAvailableException.class,
() -> accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1));
account2 = accountsManager.confirmReservedUsernameHash(reservation2.account(), USERNAME_HASH_1);
assertEquals(accountsManager.getByUsernameHash(USERNAME_HASH_1).orElseThrow().getUuid(), account2.getUuid());
assertArrayEquals(account2.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
}
@Test
void testUsernameReserveClearSetReserved()
throws InterruptedException, UsernameNotAvailableException, UsernameReservationNotFoundException {
void testUsernameSetReserveAnotherClearSetReserved()
throws InterruptedException, UsernameHashNotAvailableException, UsernameReservationNotFoundException {
Account account = accountsManager.create("+18005551111", "password", null, new AccountAttributes(),
new ArrayList<>());
account = accountsManager.setUsername(account, "n00bkiller", null);
final AccountsManager.UsernameReservation reservation = accountsManager.reserveUsername(account, "other");
account = reservation.account();
assertThat(reservation.reservedUsername()).startsWith("other");
assertThat(account.getUsername()).hasValueSatisfying(s -> s.startsWith("n00bkiller"));
// Set username hash
final AccountsManager.UsernameReservation reservation1 = accountsManager.reserveUsernameHash(account, List.of(
USERNAME_HASH_1));
account = accountsManager.confirmReservedUsernameHash(reservation1.account(), USERNAME_HASH_1);
account = accountsManager.clearUsername(account);
// Reserve another hash on the same account
final AccountsManager.UsernameReservation reservation2 = accountsManager.reserveUsernameHash(account, List.of(
USERNAME_HASH_2));
account = reservation2.account();
assertArrayEquals(account.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_2);
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
// Clear the set username hash but not the reserved one
account = accountsManager.clearUsernameHash(account);
assertThat(account.getReservedUsernameHash()).isPresent();
assertThat(account.getUsername()).isEmpty();
assertThat(account.getUsernameHash()).isEmpty();
account = accountsManager.confirmReservedUsername(account, reservation.reservedUsername(), reservation.reservationToken());
assertThat(account.getUsername()).hasValueSatisfying(s -> s.startsWith("other"));
// Confirm second reservation
account = accountsManager.confirmReservedUsernameHash(account, reservation2.reservedUsernameHash());
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_2);
}
}

View File

@@ -9,6 +9,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
@@ -21,6 +23,7 @@ import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -28,7 +31,6 @@ import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -66,9 +68,15 @@ class AccountsTest {
private static final String NUMBER_CONSTRAINT_TABLE_NAME = "numbers_test";
private static final String PNI_CONSTRAINT_TABLE_NAME = "pni_test";
private static final String USERNAME_CONSTRAINT_TABLE_NAME = "username_test";
private static final String BASE_64_URL_USERNAME_HASH_1 = "9p6Tip7BFefFOJzv4kv4GyXEYsBVfk_WbjNejdlOvQE";
private static final String BASE_64_URL_USERNAME_HASH_2 = "NLUom-CHwtemcdvOTTXdmXmzRIV7F05leS8lwkVK_vc";
private static final byte[] USERNAME_HASH_1 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_1);
private static final byte[] USERNAME_HASH_2 = Base64.getUrlDecoder().decode(BASE_64_URL_USERNAME_HASH_2);
private static final int SCAN_PAGE_SIZE = 1;
@RegisterExtension
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
.tableName(ACCOUNTS_TABLE_NAME)
@@ -118,12 +126,12 @@ class AccountsTest {
CreateTableRequest createUsernamesTableRequest = CreateTableRequest.builder()
.tableName(USERNAME_CONSTRAINT_TABLE_NAME)
.keySchema(KeySchemaElement.builder()
.attributeName(Accounts.ATTR_USERNAME)
.attributeName(Accounts.ATTR_USERNAME_HASH)
.keyType(KeyType.HASH)
.build())
.attributeDefinitions(AttributeDefinition.builder()
.attributeName(Accounts.ATTR_USERNAME)
.attributeType(ScalarAttributeType.S)
.attributeName(Accounts.ATTR_USERNAME_HASH)
.attributeType(ScalarAttributeType.B)
.build())
.provisionedThroughput(DynamoDbExtension.DEFAULT_PROVISIONED_THROUGHPUT)
.build();
@@ -614,40 +622,38 @@ class AccountsTest {
}
@Test
void testSetUsername() {
void testSwitchUsernameHashes() {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
final String username = "TeST";
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1)).isEmpty();
assertThat(accounts.getByUsername(username)).isEmpty();
accounts.setUsername(account, username);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1);
{
final Optional<Account> maybeAccount = accounts.getByUsername(username);
final Optional<Account> maybeAccount = accounts.getByUsernameHash(USERNAME_HASH_1);
assertThat(maybeAccount).hasValueSatisfying(retrievedAccount ->
assertThat(retrievedAccount.getUsername()).hasValueSatisfying(retrievedUsername ->
assertThat(retrievedUsername).isEqualTo(username)));
assertThat(retrievedAccount.getUsernameHash()).hasValueSatisfying(retrievedUsernameHash ->
assertArrayEquals(retrievedUsernameHash, USERNAME_HASH_1)));
verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(), maybeAccount.orElseThrow(), account);
}
final String secondUsername = username + "2";
accounts.reserveUsernameHash(account, USERNAME_HASH_2, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_2);
accounts.setUsername(account, secondUsername);
assertThat(accounts.getByUsername(username)).isEmpty();
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1)).isEmpty();
assertThat(dynamoDbExtension.getDynamoDbClient()
.getItem(GetItemRequest.builder()
.tableName(USERNAME_CONSTRAINT_TABLE_NAME)
.key(Map.of(Accounts.ATTR_USERNAME, AttributeValues.fromString("test")))
.key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1)))
.build())
.item()).isEmpty();
{
final Optional<Account> maybeAccount = accounts.getByUsername(secondUsername);
final Optional<Account> maybeAccount = accounts.getByUsernameHash(USERNAME_HASH_2);
assertThat(maybeAccount).isPresent();
verifyStoredState(account.getNumber(), account.getUuid(), account.getPhoneNumberIdentifier(),
@@ -656,38 +662,51 @@ class AccountsTest {
}
@Test
void testSetUsernameConflict() {
void testUsernameHashConflict() {
final Account firstAccount = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
final Account secondAccount = generateAccount("+18005559876", UUID.randomUUID(), UUID.randomUUID());
accounts.create(firstAccount);
accounts.create(secondAccount);
final String username = "test";
// first account reserves and confirms username hash
assertThatNoException().isThrownBy(() -> {
accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1);
});
assertThatNoException().isThrownBy(() -> accounts.setUsername(firstAccount, username));
final Optional<Account> maybeAccount = accounts.getByUsername(username);
final Optional<Account> maybeAccount = accounts.getByUsernameHash(USERNAME_HASH_1);
assertThat(maybeAccount).isPresent();
verifyStoredState(firstAccount.getNumber(), firstAccount.getUuid(), firstAccount.getPhoneNumberIdentifier(), maybeAccount.get(), firstAccount);
// throw an error if second account tries to reserve or confirm the same username hash
assertThatExceptionOfType(ContestedOptimisticLockException.class)
.isThrownBy(() -> accounts.setUsername(secondAccount, username));
.isThrownBy(() -> accounts.reserveUsernameHash(secondAccount, USERNAME_HASH_1, Duration.ofDays(1)));
assertThatExceptionOfType(ContestedOptimisticLockException.class)
.isThrownBy(() -> accounts.confirmUsernameHash(secondAccount, USERNAME_HASH_1));
assertThat(secondAccount.getUsername()).isEmpty();
// throw an error if first account tries to reserve or confirm the username hash that it has already confirmed
assertThatExceptionOfType(ContestedOptimisticLockException.class)
.isThrownBy(() -> accounts.reserveUsernameHash(firstAccount, USERNAME_HASH_1, Duration.ofDays(1)));
assertThatExceptionOfType(ContestedOptimisticLockException.class)
.isThrownBy(() -> accounts.confirmUsernameHash(firstAccount, USERNAME_HASH_1));
assertThat(secondAccount.getReservedUsernameHash()).isEmpty();
assertThat(secondAccount.getUsernameHash()).isEmpty();
}
@Test
void testSetUsernameVersionMismatch() {
void testConfirmUsernameHashVersionMismatch() {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
account.setVersion(account.getVersion() + 77);
assertThatExceptionOfType(ContestedOptimisticLockException.class)
.isThrownBy(() -> accounts.setUsername(account, "test"));
.isThrownBy(() -> accounts.confirmUsernameHash(account, USERNAME_HASH_1));
assertThat(account.getUsername()).isEmpty();
assertThat(account.getUsernameHash()).isEmpty();
}
@Test
@@ -695,16 +714,15 @@ class AccountsTest {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
final String username = "TeST";
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1);
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1)).isPresent();
accounts.setUsername(account, username);
assertThat(accounts.getByUsername(username)).isPresent();
accounts.clearUsernameHash(account);
accounts.clearUsername(account);
assertThat(accounts.getByUsername(username)).isEmpty();
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1)).isEmpty();
assertThat(accounts.getByAccountIdentifier(account.getUuid()))
.hasValueSatisfying(clearedAccount -> assertThat(clearedAccount.getUsername()).isEmpty());
.hasValueSatisfying(clearedAccount -> assertThat(clearedAccount.getUsernameHash()).isEmpty());
}
@Test
@@ -712,7 +730,7 @@ class AccountsTest {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
assertThatNoException().isThrownBy(() -> accounts.clearUsername(account));
assertThatNoException().isThrownBy(() -> accounts.clearUsernameHash(account));
}
@Test
@@ -720,167 +738,136 @@ class AccountsTest {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
final String username = "test";
accounts.setUsername(account, username);
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1));
accounts.confirmUsernameHash(account, USERNAME_HASH_1);
account.setVersion(account.getVersion() + 12);
assertThatExceptionOfType(ContestedOptimisticLockException.class).isThrownBy(() -> accounts.clearUsername(account));
assertThatExceptionOfType(ContestedOptimisticLockException.class).isThrownBy(() -> accounts.clearUsernameHash(account));
assertThat(account.getUsername()).hasValueSatisfying(u -> assertThat(u).isEqualTo(username));
assertArrayEquals(account.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
}
@Test
void testReservedUsername() {
void testReservedUsernameHash() {
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account1);
final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account2);
final UUID token = accounts.reserveUsername(account1, "GarfielD", Duration.ofDays(1));
assertThat(account1.getReservedUsernameHash()).get().isEqualTo(Accounts.reservedUsernameHash(account1.getUuid(), "GarfielD"));
assertThat(account1.getUsername()).isEmpty();
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1));
assertArrayEquals(account1.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
assertThat(account1.getUsernameHash()).isEmpty();
// account 2 shouldn't be able to reserve the username if it's the same when normalized
// account 2 shouldn't be able to reserve or confirm the same username hash
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.reserveUsername(account2, "gARFIELd", Duration.ofDays(1)));
() -> accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1)));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.confirmUsername(account2, "gARFIELd", UUID.randomUUID()));
assertThat(accounts.getByUsername("gARFIELd")).isEmpty();
() -> accounts.confirmUsernameHash(account2, USERNAME_HASH_1));
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1)).isEmpty();
accounts.confirmUsername(account1, "GarfielD", token);
accounts.confirmUsernameHash(account1, USERNAME_HASH_1);
assertThat(account1.getReservedUsernameHash()).isEmpty();
assertThat(account1.getUsername()).get().isEqualTo("GarfielD");
assertThat(accounts.getByUsername("GarfielD").get().getUuid()).isEqualTo(account1.getUuid());
assertArrayEquals(account1.getUsernameHash().orElseThrow(), USERNAME_HASH_1);
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).get().getUuid()).isEqualTo(account1.getUuid());
final Map<String, AttributeValue> usernameConstraintRecord = dynamoDbExtension.getDynamoDbClient()
.getItem(GetItemRequest.builder()
.tableName(USERNAME_CONSTRAINT_TABLE_NAME)
.key(Map.of(Accounts.ATTR_USERNAME, AttributeValues.fromString("garfield")))
.key(Map.of(Accounts.ATTR_USERNAME_HASH, AttributeValues.fromByteArray(USERNAME_HASH_1)))
.build())
.item();
assertThat(usernameConstraintRecord).containsKey(Accounts.ATTR_USERNAME);
assertThat(usernameConstraintRecord).containsKey(Accounts.ATTR_USERNAME_HASH);
assertThat(usernameConstraintRecord).doesNotContainKey(Accounts.ATTR_TTL);
}
@Test
void testUsernameAvailable() {
void testUsernameHashAvailable() {
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account1);
final String username = "UnSinkaBlesam";
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1));
assertThat(accounts.usernameHashAvailable(USERNAME_HASH_1)).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.empty(), USERNAME_HASH_1)).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.of(UUID.randomUUID()), USERNAME_HASH_1)).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.of(account1.getUuid()), USERNAME_HASH_1)).isTrue();
final UUID token = accounts.reserveUsername(account1, username, Duration.ofDays(1));
assertThat(accounts.usernameAvailable(username)).isFalse();
assertThat(accounts.usernameAvailable(Optional.empty(), username)).isFalse();
assertThat(accounts.usernameAvailable(Optional.of(UUID.randomUUID()), username)).isFalse();
assertThat(accounts.usernameAvailable(Optional.of(token), username)).isTrue();
accounts.confirmUsername(account1, username, token);
assertThat(accounts.usernameAvailable(username)).isFalse();
assertThat(accounts.usernameAvailable(Optional.empty(), username)).isFalse();
assertThat(accounts.usernameAvailable(Optional.of(UUID.randomUUID()), username)).isFalse();
assertThat(accounts.usernameAvailable(Optional.of(token), username)).isFalse();
}
@Test
void testReservedUsernameWrongToken() {
final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
accounts.reserveUsername(account, "grumpy", Duration.ofDays(1));
assertThat(account.getReservedUsernameHash())
.get()
.isEqualTo(Accounts.reservedUsernameHash(account.getUuid(), "grumpy"));
assertThat(account.getUsername()).isEmpty();
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.confirmUsername(account, "grumpy", UUID.randomUUID()));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.setUsername(account, "grumpy"));
accounts.confirmUsernameHash(account1, USERNAME_HASH_1);
assertThat(accounts.usernameHashAvailable(USERNAME_HASH_1)).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.empty(), USERNAME_HASH_1)).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.of(UUID.randomUUID()), USERNAME_HASH_1)).isFalse();
assertThat(accounts.usernameHashAvailable(Optional.of(account1.getUuid()), USERNAME_HASH_1)).isFalse();
}
@Test
void testReserveExpiredReservedUsername() {
void testConfirmReservedUsernameHashWrongAccountUuid() {
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account1);
final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account2);
final String username = "snowball.02";
accounts.reserveUsername(account1, username, Duration.ofDays(2));
Supplier<UUID> take = () -> accounts.reserveUsername(account2, username, Duration.ofDays(2));
for (int i = 0; i <= 2; i++) {
clock.pin(Instant.EPOCH.plus(Duration.ofDays(i)));
assertThrows(ContestedOptimisticLockException.class, take::get);
}
// after 2 days, can take the name
clock.pin(Instant.EPOCH.plus(Duration.ofDays(2)).plus(Duration.ofSeconds(1)));
final UUID token = take.get();
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(1));
assertArrayEquals(account1.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
assertThat(account1.getUsernameHash()).isEmpty();
// only account1 should be able to confirm the reserved hash
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.reserveUsername(account1, username, Duration.ofDays(2)));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.setUsername(account1, username));
accounts.confirmUsername(account2, username, token);
assertThat(accounts.getByUsername(username).get().getUuid()).isEqualTo(account2.getUuid());
() -> accounts.confirmUsernameHash(account2, USERNAME_HASH_1));
}
@Test
void testTakeExpiredReservedUsername() {
void testConfirmExpiredReservedUsernameHash() {
final Account account1 = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account1);
final Account account2 = generateAccount("+18005552222", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account2);
final String username = "simon.123";
accounts.reserveUsername(account1, username, Duration.ofDays(2));
accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2));
Runnable take = () -> accounts.setUsername(account2, username);
Runnable runnable = () -> accounts.reserveUsernameHash(account2, USERNAME_HASH_1, Duration.ofDays(1));
for (int i = 0; i <= 2; i++) {
clock.pin(Instant.EPOCH.plus(Duration.ofDays(i)));
assertThrows(ContestedOptimisticLockException.class, take::run);
assertThrows(ContestedOptimisticLockException.class, runnable::run);
}
// after 2 days, can take the name
// after 2 days, can reserve and confirm the hash
clock.pin(Instant.EPOCH.plus(Duration.ofDays(2)).plus(Duration.ofSeconds(1)));
take.run();
runnable.run();
assertEquals(account2.getReservedUsernameHash().orElseThrow(), USERNAME_HASH_1);
accounts.confirmUsernameHash(account2, USERNAME_HASH_1);
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.reserveUsername(account1, username, Duration.ofDays(2)));
() -> accounts.reserveUsernameHash(account1, USERNAME_HASH_1, Duration.ofDays(2)));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.setUsername(account1, username));
assertThat(accounts.getByUsername(username).get().getUuid()).isEqualTo(account2.getUuid());
() -> accounts.confirmUsernameHash(account1, USERNAME_HASH_1));
assertThat(accounts.getByUsernameHash(USERNAME_HASH_1).get().getUuid()).isEqualTo(account2.getUuid());
}
@Test
void testRetryReserveUsername() {
void testRetryReserveUsernameHash() {
final Account account = generateAccount("+18005551111", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
accounts.reserveUsername(account, "jorts", Duration.ofDays(2));
accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(2));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.reserveUsername(account, "jorts", Duration.ofDays(2)),
"Shouldn't be able to re-reserve same username (would extend ttl)");
() -> accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(2)),
"Shouldn't be able to re-reserve same username hash (would extend ttl)");
}
@Test
void testReserveUsernameVersionConflict() {
void testReserveConfirmUsernameHashVersionConflict() {
final Account account = generateAccount("+18005551234", UUID.randomUUID(), UUID.randomUUID());
accounts.create(account);
account.setVersion(account.getVersion() + 12);
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.reserveUsername(account, "salem", Duration.ofDays(1)));
() -> accounts.reserveUsernameHash(account, USERNAME_HASH_1, Duration.ofDays(1)));
assertThrows(ContestedOptimisticLockException.class,
() -> accounts.setUsername(account, "salem"));
() -> accounts.confirmUsernameHash(account, USERNAME_HASH_1));
assertThat(account.getReservedUsernameHash()).isEmpty();
assertThat(account.getUsernameHash()).isEmpty();
}
private Device generateDevice(long id) {

View File

@@ -1,69 +0,0 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.storage;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.UUID;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
class ProhibitedUsernamesTest {
private static final String RESERVED_USERNAMES_TABLE_NAME = "reserved_usernames_test";
@RegisterExtension
static DynamoDbExtension dynamoDbExtension = DynamoDbExtension.builder()
.tableName(RESERVED_USERNAMES_TABLE_NAME)
.hashKey(ProhibitedUsernames.KEY_PATTERN)
.attributeDefinition(AttributeDefinition.builder()
.attributeName(ProhibitedUsernames.KEY_PATTERN)
.attributeType(ScalarAttributeType.S)
.build())
.build();
private static final UUID RESERVED_FOR_UUID = UUID.randomUUID();
private ProhibitedUsernames prohibitedUsernames;
@BeforeEach
void setUp() {
prohibitedUsernames =
new ProhibitedUsernames(dynamoDbExtension.getDynamoDbClient(), RESERVED_USERNAMES_TABLE_NAME);
}
@ParameterizedTest
@MethodSource
void isReserved(final String username, final UUID uuid, final boolean expectReserved) {
prohibitedUsernames.prohibitUsername(".*myusername.*", RESERVED_FOR_UUID);
prohibitedUsernames.prohibitUsername("^foobar$", RESERVED_FOR_UUID);
assertEquals(expectReserved, prohibitedUsernames.isProhibited(username, uuid));
}
private static Stream<Arguments> isReserved() {
return Stream.of(
Arguments.of("myusername", UUID.randomUUID(), true),
Arguments.of("myusername", RESERVED_FOR_UUID, false),
Arguments.of("thyusername", UUID.randomUUID(), false),
Arguments.of("somemyusername", UUID.randomUUID(), true),
Arguments.of("myusernamesome", UUID.randomUUID(), true),
Arguments.of("somemyusernamesome", UUID.randomUUID(), true),
Arguments.of("MYUSERNAME", UUID.randomUUID(), true),
Arguments.of("foobar", UUID.randomUUID(), true),
Arguments.of("foobar", RESERVED_FOR_UUID, false),
Arguments.of("somefoobar", UUID.randomUUID(), false),
Arguments.of("foobarsome", UUID.randomUUID(), false),
Arguments.of("somefoobarsome", UUID.randomUUID(), false),
Arguments.of("FOOBAR", UUID.randomUUID(), true));
}
}

View File

@@ -113,7 +113,7 @@ public class AccountsHelper {
case "getUuid" -> when(updatedAccount.getUuid()).thenAnswer(stubbing);
case "getPhoneNumberIdentifier" -> when(updatedAccount.getPhoneNumberIdentifier()).thenAnswer(stubbing);
case "getNumber" -> when(updatedAccount.getNumber()).thenAnswer(stubbing);
case "getUsername" -> when(updatedAccount.getUsername()).thenAnswer(stubbing);
case "getUsername" -> when(updatedAccount.getUsernameHash()).thenAnswer(stubbing);
case "getDevices" -> when(updatedAccount.getDevices()).thenAnswer(stubbing);
case "getDevice" -> when(updatedAccount.getDevice(stubbing.getInvocation().getArgument(0))).thenAnswer(stubbing);
case "getMasterDevice" -> when(updatedAccount.getMasterDevice()).thenAnswer(stubbing);

View File

@@ -1,145 +0,0 @@
package org.whispersystems.textsecuregcm.tests.util;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.whispersystems.textsecuregcm.storage.UsernameNotAvailableException;
import org.whispersystems.textsecuregcm.util.UsernameGenerator;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
public class UsernameGeneratorTest {
private static final Duration TTL = Duration.ofMinutes(5);
@ParameterizedTest(name = "[{index}]:{0} ({2})")
@MethodSource
public void nicknameValidation(String nickname, boolean valid, String testCaseName) {
assertThat(UsernameGenerator.isValidNickname(nickname)).isEqualTo(valid);
}
static Stream<Arguments> nicknameValidation() {
return Stream.of(
Arguments.of("Test", true, "upper case"),
Arguments.of("tesT", true, "upper case"),
Arguments.of("te-st", false, "illegal character"),
Arguments.of("ab\uD83D\uDC1B", false, "illegal character"),
Arguments.of("1test", false, "illegal start"),
Arguments.of("test#123", false, "illegal character"),
Arguments.of("test.123", false, "illegal character"),
Arguments.of("ab", false, "too short"),
Arguments.of("", false, ""),
Arguments.of("_123456789_123456789_123456789123", false, "33 characters"),
Arguments.of("_test", true, ""),
Arguments.of("test", true, ""),
Arguments.of("test123", true, ""),
Arguments.of("abc", true, ""),
Arguments.of("_123456789_123456789_12345678912", true, "32 characters")
);
}
@ParameterizedTest(name="[{index}]: {0}")
@MethodSource
public void nonStandardUsernames(final String username, final boolean isStandard) {
assertThat(UsernameGenerator.isStandardFormat(username)).isEqualTo(isStandard);
}
static Stream<Arguments> nonStandardUsernames() {
return Stream.of(
Arguments.of("Test.123", true),
Arguments.of("test.-123", false),
Arguments.of("test.0", false),
Arguments.of("test.", false),
Arguments.of("test.1_00", false),
Arguments.of("test.1", true),
Arguments.of("abc.1234", true)
);
}
@Test
public void zeroPadDiscriminators() {
final UsernameGenerator generator = new UsernameGenerator(4, 5, 1, TTL);
assertThat(generator.fromParts("test", 1)).isEqualTo("test.0001");
assertThat(generator.fromParts("test", 123)).isEqualTo("test.0123");
assertThat(generator.fromParts("test", 9999)).isEqualTo("test.9999");
assertThat(generator.fromParts("test", 99999)).isEqualTo("test.99999");
}
@Test
public void expectedWidth() throws UsernameNotAvailableException {
String username = new UsernameGenerator(1, 6, 1, TTL).generateAvailableUsername("test", t -> true);
assertThat(extractDiscriminator(username)).isGreaterThan(0).isLessThan(10);
username = new UsernameGenerator(2, 6, 1, TTL).generateAvailableUsername("test", t -> true);
assertThat(extractDiscriminator(username)).isGreaterThan(0).isLessThan(100);
}
@Test
public void expandDiscriminator() throws UsernameNotAvailableException {
UsernameGenerator ug = new UsernameGenerator(1, 6, 10, TTL);
final String username = ug.generateAvailableUsername("test", allowDiscriminator(d -> d >= 10000));
int discriminator = extractDiscriminator(username);
assertThat(discriminator).isGreaterThanOrEqualTo(10000).isLessThan(100000);
}
@Test
public void expandDiscriminatorToMax() throws UsernameNotAvailableException {
UsernameGenerator ug = new UsernameGenerator(1, 6, 10, TTL);
final String username = ug.generateAvailableUsername("test", allowDiscriminator(d -> d >= 100000));
int discriminator = extractDiscriminator(username);
assertThat(discriminator).isGreaterThanOrEqualTo(100000).isLessThan(1000000);
}
@Test
public void exhaustDiscriminator() {
UsernameGenerator ug = new UsernameGenerator(1, 6, 10, TTL);
Assertions.assertThrows(UsernameNotAvailableException.class, () -> {
// allow greater than our max width
ug.generateAvailableUsername("test", allowDiscriminator(d -> d >= 1000000));
});
}
@Test
public void randomCoverageMinWidth() throws UsernameNotAvailableException {
UsernameGenerator ug = new UsernameGenerator(1, 6, 10, TTL);
final Set<Integer> seen = new HashSet<>();
for (int i = 0; i < 1000 && seen.size() < 9; i++) {
seen.add(extractDiscriminator(ug.generateAvailableUsername("test", ignored -> true)));
}
// after 1K iterations, probability of a missed value is (9/10)^999
assertThat(seen.size()).isEqualTo(9);
assertThat(seen).allMatch(i -> i > 0 && i < 10);
}
@Test
public void randomCoverageMidWidth() throws UsernameNotAvailableException {
UsernameGenerator ug = new UsernameGenerator(1, 6, 10, TTL);
final Set<Integer> seen = new HashSet<>();
for (int i = 0; i < 100000 && seen.size() < 90; i++) {
seen.add(extractDiscriminator(ug.generateAvailableUsername("test", allowDiscriminator(d -> d >= 10))));
}
// after 100K iterations, probability of a missed value is (99/100)^99999
assertThat(seen.size()).isEqualTo(90);
assertThat(seen).allMatch(i -> i >= 10 && i < 100);
}
private static Predicate<String> allowDiscriminator(Predicate<Integer> p) {
return username -> p.test(extractDiscriminator(username));
}
private static int extractDiscriminator(final String username) {
return Integer.parseInt(username.substring(username.indexOf(UsernameGenerator.SEPARATOR) + 1));
}
}

View File

@@ -1,17 +0,0 @@
package org.whispersystems.textsecuregcm.tests.util;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.util.UsernameNormalizer;
import static org.assertj.core.api.Assertions.assertThat;
public class UsernameNormalizerTest {
@Test
public void usernameNormalization() {
assertThat(UsernameNormalizer.normalize("TeST")).isEqualTo("test");
assertThat(UsernameNormalizer.normalize("TeST_")).isEqualTo("test_");
assertThat(UsernameNormalizer.normalize("TeST_.123")).isEqualTo("test_.123");
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class NicknameValidatorTest {
@ParameterizedTest
@MethodSource
void isValid(final String username, final boolean expectValid) {
final NicknameValidator nicknameValidator = new NicknameValidator();
assertEquals(expectValid, nicknameValidator.isValid(username, null));
}
private static Stream<Arguments> isValid() {
return Stream.of(
Arguments.of("test", true),
Arguments.of("_test", true),
Arguments.of("test123", true),
Arguments.of("a", false), // Too short
Arguments.of("thisisareallyreallyreallylongusernamethatwewouldnotalllow", false),
Arguments.of("illegal character", false),
Arguments.of("0test", false), // Illegal first character
Arguments.of("pаypal", false), // Unicode confusable characters
Arguments.of("test\uD83D\uDC4E", false), // Emoji
Arguments.of(" ", false),
Arguments.of("", false),
Arguments.of(null, false)
);
}
}