test classes moved to same packages with components they test

This commit is contained in:
Sergey Skrobotov
2023-07-17 12:58:10 -07:00
parent b8d8d349f4
commit 352e1b2249
42 changed files with 84 additions and 136 deletions

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.whispersystems.textsecuregcm.util.HmacUtils.hmac256TruncatedToHexString;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.util.MockUtils;
import org.whispersystems.textsecuregcm.util.MutableClock;
class ExternalServiceCredentialsGeneratorTest {
private static final String PREFIX = "prefix";
private static final String E164 = "+14152222222";
private static final long TIME_SECONDS = 12345;
private static final long TIME_MILLIS = TimeUnit.SECONDS.toMillis(TIME_SECONDS);
private static final String TIME_SECONDS_STRING = Long.toString(TIME_SECONDS);
private static final String USERNAME_TIMESTAMP = PREFIX + ":" + Instant.ofEpochSecond(TIME_SECONDS).truncatedTo(ChronoUnit.DAYS).getEpochSecond();
private static final MutableClock clock = MockUtils.mutableClock(TIME_MILLIS);
private static final ExternalServiceCredentialsGenerator standardGenerator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withClock(clock)
.build();
private static final ExternalServiceCredentials standardCredentials = standardGenerator.generateFor(E164);
private static final ExternalServiceCredentialsGenerator usernameIsTimestampGenerator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withUsernameTimestampTruncatorAndPrefix(timestamp -> timestamp.truncatedTo(ChronoUnit.DAYS), PREFIX)
.withClock(clock)
.build();
private static final ExternalServiceCredentials usernameIsTimestampCredentials = usernameIsTimestampGenerator.generateWithTimestampAsUsername();
@BeforeEach
public void before() throws Exception {
clock.setTimeMillis(TIME_MILLIS);
}
@Test
void testInvalidConstructor() {
assertThrows(RuntimeException.class, () -> ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withUsernameTimestampTruncatorAndPrefix(null, PREFIX)
.build());
assertThrows(RuntimeException.class, () -> ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withUsernameTimestampTruncatorAndPrefix(timestamp -> timestamp.truncatedTo(ChronoUnit.DAYS), null)
.build());
}
@Test
void testGenerateDerivedUsername() {
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.withUserDerivationKey(new byte[32])
.build();
final ExternalServiceCredentials credentials = generator.generateFor(E164);
assertNotEquals(credentials.username(), E164);
assertFalse(credentials.password().startsWith(E164));
assertEquals(credentials.password().split(":").length, 3);
}
@Test
void testGenerateNoDerivedUsername() {
assertEquals(standardCredentials.username(), E164);
assertTrue(standardCredentials.password().startsWith(E164));
assertEquals(standardCredentials.password().split(":").length, 3);
}
@Test
public void testNotPrependUsername() throws Exception {
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator
.builder(new byte[32])
.prependUsername(false)
.withClock(clock)
.build();
final ExternalServiceCredentials credentials = generator.generateFor(E164);
assertEquals(credentials.username(), E164);
assertTrue(credentials.password().startsWith(TIME_SECONDS_STRING));
assertEquals(credentials.password().split(":").length, 2);
}
@Test
public void testWithUsernameIsTimestamp() {
assertEquals(USERNAME_TIMESTAMP, usernameIsTimestampCredentials.username());
final String[] passwordComponents = usernameIsTimestampCredentials.password().split(":");
assertEquals(USERNAME_TIMESTAMP, passwordComponents[0] + ":" + passwordComponents[1]);
assertEquals(hmac256TruncatedToHexString(new byte[32], USERNAME_TIMESTAMP, 10), passwordComponents[2]);
}
@Test
public void testValidateValid() throws Exception {
assertEquals(standardGenerator.validateAndGetTimestamp(standardCredentials).orElseThrow(), TIME_SECONDS);
}
@Test
public void testValidateValidWithUsernameIsTimestamp() {
final long expectedTimestamp = Instant.ofEpochSecond(TIME_SECONDS).truncatedTo(ChronoUnit.DAYS).getEpochSecond();
assertEquals(expectedTimestamp, usernameIsTimestampGenerator.validateAndGetTimestamp(usernameIsTimestampCredentials).orElseThrow());
}
@Test
public void testValidateInvalid() throws Exception {
final ExternalServiceCredentials corruptedStandardUsername = new ExternalServiceCredentials(
standardCredentials.username(), standardCredentials.password().replace(E164, E164 + "0"));
final ExternalServiceCredentials corruptedStandardTimestamp = new ExternalServiceCredentials(
standardCredentials.username(), standardCredentials.password().replace(TIME_SECONDS_STRING, TIME_SECONDS_STRING + "0"));
final ExternalServiceCredentials corruptedStandardPassword = new ExternalServiceCredentials(
standardCredentials.username(), standardCredentials.password() + "0");
final ExternalServiceCredentials corruptedUsernameTimestamp = new ExternalServiceCredentials(
usernameIsTimestampCredentials.username(), usernameIsTimestampCredentials.password().replace(USERNAME_TIMESTAMP, USERNAME_TIMESTAMP
+ "0"));
final ExternalServiceCredentials corruptedUsernameTimestampPassword = new ExternalServiceCredentials(
usernameIsTimestampCredentials.username(), usernameIsTimestampCredentials.password() + "0");
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardUsername).isEmpty());
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardTimestamp).isEmpty());
assertTrue(standardGenerator.validateAndGetTimestamp(corruptedStandardPassword).isEmpty());
assertTrue(usernameIsTimestampGenerator.validateAndGetTimestamp(corruptedUsernameTimestamp).isEmpty());
assertTrue(usernameIsTimestampGenerator.validateAndGetTimestamp(corruptedUsernameTimestampPassword).isEmpty());
}
@Test
public void testValidateWithExpiration() throws Exception {
final long elapsedSeconds = 10000;
clock.incrementSeconds(elapsedSeconds);
assertEquals(standardGenerator.validateAndGetTimestamp(standardCredentials, elapsedSeconds + 1).orElseThrow(), TIME_SECONDS);
assertTrue(standardGenerator.validateAndGetTimestamp(standardCredentials, elapsedSeconds - 1).isEmpty());
}
@Test
public void testGetIdentityFromSignature() {
final String identity = standardGenerator.identityFromSignature(standardCredentials.password()).orElseThrow();
assertEquals(E164, identity);
}
@Test
public void testGetIdentityFromSignatureIsTimestamp() {
final String identity = usernameIsTimestampGenerator.identityFromSignature(usernameIsTimestampCredentials.password()).orElseThrow();
assertEquals(USERNAME_TIMESTAMP, identity);
}
@Test
public void testTruncateLength() throws Exception {
final ExternalServiceCredentialsGenerator generator = ExternalServiceCredentialsGenerator.builder(new byte[32])
.withUserDerivationKey(new byte[32])
.withDerivedUsernameTruncateLength(14)
.build();
final ExternalServiceCredentials creds = generator.generateFor(E164);
assertEquals(14*2 /* 2 chars per byte, because hex */, creds.username().length());
assertEquals("805b84df7eff1e8fe1baf0c6e838", creds.username());
generator.validateAndGetTimestamp(creds);
}
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.apache.commons.lang3.RandomUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsSelector.CredentialInfo;
import org.whispersystems.textsecuregcm.util.MockUtils;
import org.whispersystems.textsecuregcm.util.MutableClock;
public class ExternalServiceCredentialsSelectorTest {
private static final UUID UUID1 = UUID.randomUUID();
private static final UUID UUID2 = UUID.randomUUID();
private static final MutableClock CLOCK = MockUtils.mutableClock(TimeUnit.DAYS.toSeconds(1));
private static final ExternalServiceCredentialsGenerator GEN1 =
ExternalServiceCredentialsGenerator
.builder(RandomUtils.nextBytes(32))
.prependUsername(true)
.withClock(CLOCK)
.build();
private static final ExternalServiceCredentialsGenerator GEN2 =
ExternalServiceCredentialsGenerator
.builder(RandomUtils.nextBytes(32))
.withUserDerivationKey(RandomUtils.nextBytes(32))
.prependUsername(false)
.withDerivedUsernameTruncateLength(16)
.withClock(CLOCK)
.build();
private static ExternalServiceCredentials atTime(
final ExternalServiceCredentialsGenerator gen,
final long deltaMillis,
final UUID identity) {
final Instant old = CLOCK.instant();
try {
CLOCK.incrementMillis(deltaMillis);
return gen.generateForUuid(identity);
} finally {
CLOCK.setTimeInstant(old);
}
}
private static String token(final ExternalServiceCredentials cred) {
return cred.username() + ":" + cred.password();
}
@Test
void single() {
final ExternalServiceCredentials cred = GEN1.generateForUuid(UUID1);
var result = ExternalServiceCredentialsSelector.check(
List.of(token(cred)), GEN1, TimeUnit.MINUTES.toSeconds(1));
assertThat(result).singleElement()
.matches(CredentialInfo::valid)
.matches(info -> info.credentials().equals(cred));
}
@Test
void multipleUsernames() {
final ExternalServiceCredentials cred1New = GEN1.generateForUuid(UUID1);
final ExternalServiceCredentials cred1Old = atTime(GEN1, -1, UUID1);
final ExternalServiceCredentials cred2New = GEN1.generateForUuid(UUID2);
final ExternalServiceCredentials cred2Old = atTime(GEN1, -1, UUID2);
final List<String> tokens = Stream.of(cred1New, cred1Old, cred2New, cred2Old)
.map(ExternalServiceCredentialsSelectorTest::token)
.toList();
final List<CredentialInfo> result = ExternalServiceCredentialsSelector.check(tokens, GEN1,
TimeUnit.MINUTES.toSeconds(1));
assertThat(result).hasSize(4);
assertThat(result).filteredOn(CredentialInfo::valid)
.hasSize(2)
.map(CredentialInfo::credentials)
.containsExactlyInAnyOrder(cred1New, cred2New);
assertThat(result).filteredOn(info -> !info.valid())
.map(CredentialInfo::token)
.containsExactlyInAnyOrder(token(cred1Old), token(cred2Old));
}
@Test
void multipleGenerators() {
final ExternalServiceCredentials gen1Cred = GEN1.generateForUuid(UUID1);
final ExternalServiceCredentials gen2Cred = GEN2.generateForUuid(UUID1);
final List<CredentialInfo> result = ExternalServiceCredentialsSelector.check(
List.of(token(gen1Cred), token(gen2Cred)),
GEN2,
TimeUnit.MINUTES.toSeconds(1));
assertThat(result)
.hasSize(2)
.filteredOn(CredentialInfo::valid)
.singleElement()
.matches(info -> info.credentials().equals(gen2Cred));
assertThat(result)
.filteredOn(info -> !info.valid())
.singleElement()
.matches(info -> info.token().equals(token(gen1Cred)));
}
@ParameterizedTest
@MethodSource
void invalidCredentials(final String invalidCredential) {
final ExternalServiceCredentials validCredential = GEN1.generateForUuid(UUID1);
var result = ExternalServiceCredentialsSelector.check(
List.of(invalidCredential, token(validCredential)), GEN1, TimeUnit.MINUTES.toSeconds(1));
assertThat(result).hasSize(2);
assertThat(result).filteredOn(CredentialInfo::valid).singleElement()
.matches(info -> info.credentials().equals(validCredential));
assertThat(result).filteredOn(info -> !info.valid()).singleElement()
.matches(info -> info.token().equals(invalidCredential));
}
static Stream<String> invalidCredentials() {
return Stream.of(
"blah:blah",
token(atTime(GEN1, -TimeUnit.MINUTES.toSeconds(2), UUID1)), // too old
"nocolon",
"nothingaftercolon:",
":nothingbeforecolon",
token(GEN2.generateForUuid(UUID1))
);
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.Base64;
import java.util.Optional;
import javax.ws.rs.WebApplicationException;
import org.junit.jupiter.api.Test;
import org.whispersystems.textsecuregcm.storage.Account;
class OptionalAccessTest {
@Test
void testUnidentifiedMissingTarget() {
try {
OptionalAccess.verify(Optional.empty(), Optional.empty(), Optional.empty());
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
@Test
void testUnidentifiedMissingTargetDevice() {
Account account = mock(Account.class);
when(account.isEnabled()).thenReturn(true);
when(account.getDevice(eq(10))).thenReturn(Optional.empty());
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account), "10");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
@Test
void testUnidentifiedBadTargetDevice() {
Account account = mock(Account.class);
when(account.isEnabled()).thenReturn(true);
when(account.getDevice(eq(10))).thenReturn(Optional.empty());
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account), "$$");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 422);
}
}
@Test
void testUnidentifiedBadCode() {
Account account = mock(Account.class);
when(account.isEnabled()).thenReturn(true);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("5678".getBytes()))), Optional.of(account));
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
@Test
void testIdentifiedMissingTarget() {
Account account = mock(Account.class);
when(account.isEnabled()).thenReturn(true);
try {
OptionalAccess.verify(Optional.of(account), Optional.empty(), Optional.empty());
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 404);
}
}
@Test
void testUnsolicitedBadTarget() {
Account account = mock(Account.class);
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
when(account.isEnabled()).thenReturn(true);
try {
OptionalAccess.verify(Optional.empty(), Optional.empty(), Optional.of(account));
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
@Test
void testUnsolicitedGoodTarget() {
Account account = mock(Account.class);
Anonymous random = mock(Anonymous.class);
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(true);
when(account.isEnabled()).thenReturn(true);
OptionalAccess.verify(Optional.empty(), Optional.of(random), Optional.of(account));
}
@Test
void testUnidentifiedGoodTarget() {
Account account = mock(Account.class);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
when(account.isEnabled()).thenReturn(true);
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account));
}
@Test
void testUnidentifiedInactive() {
Account account = mock(Account.class);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
when(account.isEnabled()).thenReturn(false);
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.getEncoder().encodeToString("1234".getBytes()))), Optional.of(account));
throw new AssertionError();
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
@Test
void testIdentifiedGoodTarget() {
Account source = mock(Account.class);
Account target = mock(Account.class);
when(target.isEnabled()).thenReturn(true);
OptionalAccess.verify(Optional.of(source), Optional.empty(), Optional.of(target));
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.auth;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import org.junit.jupiter.api.Test;
class SaltedTokenHashTest {
@Test
void testCreating() {
SaltedTokenHash credentials = SaltedTokenHash.generateFor("mypassword");
assertThat(credentials.salt()).isNotEmpty();
assertThat(credentials.hash()).isNotEmpty();
assertThat(credentials.hash().length()).isEqualTo(66);
}
@Test
void testMatching() {
SaltedTokenHash credentials = SaltedTokenHash.generateFor("mypassword");
SaltedTokenHash provided = new SaltedTokenHash(credentials.hash(), credentials.salt());
assertThat(provided.verify("mypassword")).isTrue();
}
@Test
void testMisMatching() {
SaltedTokenHash credentials = SaltedTokenHash.generateFor("mypassword");
SaltedTokenHash provided = new SaltedTokenHash(credentials.hash(), credentials.salt());
assertThat(provided.verify("wrong")).isFalse();
}
}