diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index b100d91a2..16d398507 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -86,7 +86,6 @@ import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.controllers.DirectoryController; import org.whispersystems.textsecuregcm.controllers.DirectoryV2Controller; import org.whispersystems.textsecuregcm.controllers.DonationController; -import org.whispersystems.textsecuregcm.controllers.GroupController; import org.whispersystems.textsecuregcm.controllers.KeepAliveController; import org.whispersystems.textsecuregcm.controllers.KeysController; import org.whispersystems.textsecuregcm.controllers.MessageController; @@ -625,14 +624,13 @@ public class WhisperServerService extends Application endRedemptionTime) { throw new WebApplicationException(Response.Status.BAD_REQUEST); } - if (endRedemptionTime > Util.currentDaysSinceEpoch() + 7) { + final int currentDaysSinceEpoch = Util.currentDaysSinceEpoch(clock); + if (endRedemptionTime > currentDaysSinceEpoch + 7) { throw new WebApplicationException(Response.Status.BAD_REQUEST); } - if (startRedemptionTime < Util.currentDaysSinceEpoch()) { + if (startRedemptionTime < currentDaysSinceEpoch) { throw new WebApplicationException(Response.Status.BAD_REQUEST); } @@ -98,4 +115,42 @@ public class CertificateController { return new GroupCredentials(credentials); } + @Timed + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/auth/group") + public GroupCredentials getGroupAuthenticationCredentials( + @Auth AuthenticatedAccount auth, + @QueryParam("redemptionStartSeconds") int startSeconds, + @QueryParam("redemptionEndSeconds") int endSeconds) { + + final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS); + final Instant redemptionStart = Instant.ofEpochSecond(startSeconds); + final Instant redemptionEnd = Instant.ofEpochSecond(endSeconds); + + if (redemptionStart.isAfter(redemptionEnd) || + redemptionStart.isBefore(startOfDay) || + redemptionEnd.isAfter(startOfDay.plus(MAX_REDEMPTION_DURATION)) || + !redemptionStart.equals(redemptionStart.truncatedTo(ChronoUnit.DAYS)) || + !redemptionEnd.equals(redemptionEnd.truncatedTo(ChronoUnit.DAYS))) { + + throw new BadRequestException(); + } + + final List credentials = new ArrayList<>(); + + Instant redemption = redemptionStart; + + UUID aci = auth.getAccount().getUuid(); + UUID pni = auth.getAccount().getPhoneNumberIdentifier(); + while (!redemption.isAfter(redemptionEnd)) { + credentials.add(new GroupCredentials.GroupCredential( + serverZkAuthOperations.issueAuthCredentialWithPni(aci, pni, redemption).serialize(), + (int) redemption.getEpochSecond())); + + redemption = redemption.plus(Duration.ofDays(1)); + } + + return new GroupCredentials(credentials); + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/GroupController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/GroupController.java deleted file mode 100644 index abade56e4..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/GroupController.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2013-2022 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.controllers; - -import com.codahale.metrics.annotation.Timed; -import com.google.common.annotations.VisibleForTesting; -import io.dropwizard.auth.Auth; -import javax.ws.rs.BadRequestException; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; -import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations; -import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; -import org.whispersystems.textsecuregcm.entities.GroupCredentials; -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.List; - -@Path("/v1/group") -public class GroupController { - - private final ServerZkAuthOperations serverZkAuthOperations; - private final Clock clock; - - @VisibleForTesting - static final Duration MAX_REDEMPTION_DURATION = Duration.ofDays(7); - - public GroupController(final ServerZkAuthOperations serverZkAuthOperations) { - this(serverZkAuthOperations, Clock.systemUTC()); - } - - @VisibleForTesting - GroupController(final ServerZkAuthOperations serverZkAuthOperations, final Clock clock) { - this.serverZkAuthOperations = serverZkAuthOperations; - this.clock = clock; - } - - @Timed - @GET - @Produces(MediaType.APPLICATION_JSON) - @Path("/auth") - public GroupCredentials getGroupAuthenticationCredentials(@Auth AuthenticatedAccount auth, - @QueryParam("redemptionStartSeconds") int startSeconds, - @QueryParam("redemptionEndSeconds") int endSeconds) { - - final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS); - final Instant redemptionStart = Instant.ofEpochSecond(startSeconds); - final Instant redemptionEnd = Instant.ofEpochSecond(endSeconds); - - if (redemptionStart.isAfter(redemptionEnd) || - redemptionStart.isBefore(startOfDay) || - redemptionEnd.isAfter(startOfDay.plus(MAX_REDEMPTION_DURATION)) || - !redemptionStart.equals(redemptionStart.truncatedTo(ChronoUnit.DAYS)) || - !redemptionEnd.equals(redemptionEnd.truncatedTo(ChronoUnit.DAYS))) { - - throw new BadRequestException(); - } - - final List credentials = new ArrayList<>(); - - Instant redemption = redemptionStart; - - while (!redemption.isAfter(redemptionEnd)) { - credentials.add(new GroupCredentials.GroupCredential(serverZkAuthOperations.issueAuthCredentialWithPni( - auth.getAccount().getUuid(), - auth.getAccount().getPhoneNumberIdentifier(), - redemption).serialize(), - (int) redemption.getEpochSecond())); - - redemption = redemption.plus(Duration.ofDays(1)); - } - - return new GroupCredentials(credentials); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java index 66fc2d039..4591d8f7f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/util/Util.java @@ -27,6 +27,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.Nonnull; public class Util { @@ -166,8 +167,18 @@ public class Util { return (int) value; } + public static int currentDaysSinceEpoch(@Nonnull Clock clock) { + return toIntExact(clock.millis() / 1000 / 60/ 60 / 24); + } + + /** + * Returns the current number of days since the epoch. + * + * @deprecated use {@link #currentDaysSinceEpoch(Clock)} instead + */ + @Deprecated public static int currentDaysSinceEpoch() { - return Util.toIntExact(System.currentTimeMillis() / 1000 / 60 / 60 / 24); + return currentDaysSinceEpoch(Clock.systemUTC()); } public static void sleep(long i) { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/GroupControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/GroupControllerTest.java deleted file mode 100644 index f4f772ed4..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/GroupControllerTest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2013-2022 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.controllers; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.google.common.collect.ImmutableSet; -import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; -import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; -import io.dropwizard.testing.junit5.ResourceExtension; -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; -import java.time.ZoneId; -import java.time.temporal.ChronoUnit; -import java.util.stream.Stream; -import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.signal.libsignal.zkgroup.ServerSecretParams; -import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse; -import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations; -import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations; -import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; -import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount; -import org.whispersystems.textsecuregcm.entities.GroupCredentials; -import org.whispersystems.textsecuregcm.tests.util.AuthHelper; -import org.whispersystems.textsecuregcm.util.SystemMapper; -import javax.ws.rs.core.Response; - -@ExtendWith(DropwizardExtensionsSupport.class) -class GroupControllerTest { - - private static final ServerSecretParams SERVER_SECRET_PARAMS = ServerSecretParams.generate(); - private static final ServerZkAuthOperations SERVER_ZK_AUTH_OPERATIONS = new ServerZkAuthOperations(SERVER_SECRET_PARAMS); - - private static final Clock CLOCK = Clock.fixed(Instant.now(), ZoneId.systemDefault()); - - private static final ResourceExtension RESOURCE_EXTENSION = ResourceExtension.builder() - .addProvider(AuthHelper.getAuthFilter()) - .addProvider(new PolymorphicAuthValueFactoryProvider.Binder<>( - ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) - .setMapper(SystemMapper.getMapper()) - .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) - .addResource(new GroupController(SERVER_ZK_AUTH_OPERATIONS, CLOCK)) - .build(); - - @Test - void testGetSingleGroupCredential() { - final Instant startOfDay = CLOCK.instant().truncatedTo(ChronoUnit.DAYS); - - final GroupCredentials credentials = RESOURCE_EXTENSION.getJerseyTest() - .target("/v1/group/auth") - .queryParam("redemptionStartSeconds", startOfDay.getEpochSecond()) - .queryParam("redemptionEndSeconds", startOfDay.getEpochSecond()) - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .get(GroupCredentials.class); - - assertEquals(1, credentials.getCredentials().size()); - assertEquals(startOfDay.getEpochSecond(), credentials.getCredentials().get(0).getRedemptionTime()); - - final ClientZkAuthOperations clientZkAuthOperations = - new ClientZkAuthOperations(SERVER_SECRET_PARAMS.getPublicParams()); - - assertDoesNotThrow(() -> { - clientZkAuthOperations.receiveAuthCredentialWithPni(AuthHelper.VALID_UUID, - AuthHelper.VALID_PNI, - (int) startOfDay.getEpochSecond(), - new AuthCredentialWithPniResponse(credentials.getCredentials().get(0).getCredential())); - }); - } - - @Test - void testGetWeekLongGroupCredentials() { - final Instant startOfDay = CLOCK.instant().truncatedTo(ChronoUnit.DAYS); - - final GroupCredentials credentials = RESOURCE_EXTENSION.getJerseyTest() - .target("/v1/group/auth") - .queryParam("redemptionStartSeconds", startOfDay.getEpochSecond()) - .queryParam("redemptionEndSeconds", startOfDay.plus(Duration.ofDays(7)).getEpochSecond()) - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .get(GroupCredentials.class); - - assertEquals(8, credentials.getCredentials().size()); - - final ClientZkAuthOperations clientZkAuthOperations = - new ClientZkAuthOperations(SERVER_SECRET_PARAMS.getPublicParams()); - - for (int i = 0; i < 8; i++) { - final Instant redemptionTime = startOfDay.plus(Duration.ofDays(i)); - assertEquals(redemptionTime.getEpochSecond(), credentials.getCredentials().get(i).getRedemptionTime()); - - final int index = i; - - assertDoesNotThrow(() -> { - clientZkAuthOperations.receiveAuthCredentialWithPni(AuthHelper.VALID_UUID, - AuthHelper.VALID_PNI, - redemptionTime.getEpochSecond(), - new AuthCredentialWithPniResponse(credentials.getCredentials().get(index).getCredential())); - }); - } - } - - @ParameterizedTest - @MethodSource - void testBadRedemptionTimes(final Instant redemptionStart, final Instant redemptionEnd) { - final Response response = RESOURCE_EXTENSION.getJerseyTest() - .target("/v1/group/auth") - .queryParam("redemptionStartSeconds", redemptionStart.getEpochSecond()) - .queryParam("redemptionEndSeconds", redemptionEnd.getEpochSecond()) - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .get(); - - assertEquals(400, response.getStatus()); - } - - private static Stream testBadRedemptionTimes() { - return Stream.of( - // Start is after end - Arguments.of(CLOCK.instant().plus(Duration.ofDays(1)), CLOCK.instant()), - - // Start is in the past - Arguments.of(CLOCK.instant().minus(Duration.ofDays(1)), CLOCK.instant()), - - // End is too far in the future - Arguments.of(CLOCK.instant(), CLOCK.instant().plus(GroupController.MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1))), - - // Start is not at a day boundary - Arguments.of(CLOCK.instant().plusSeconds(17), CLOCK.instant().plus(Duration.ofDays(1))), - - // End is not at a day boundary - Arguments.of(CLOCK.instant(), CLOCK.instant().plusSeconds(17)) - ); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/CertificateControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/CertificateControllerTest.java index e2dcecbeb..c4b83b787 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/CertificateControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/CertificateControllerTest.java @@ -9,6 +9,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -17,16 +18,26 @@ import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; import io.dropwizard.testing.junit5.ResourceExtension; import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; import java.util.Base64; +import java.util.stream.Stream; import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.signal.libsignal.protocol.ecc.Curve; import org.signal.libsignal.zkgroup.ServerSecretParams; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse; +import org.signal.libsignal.zkgroup.auth.AuthCredentialWithPniResponse; import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations; import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations; import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount; @@ -56,6 +67,7 @@ class CertificateControllerTest { private static final ServerSecretParams serverSecretParams = ServerSecretParams.generate(); private static final CertificateGenerator certificateGenerator; private static final ServerZkAuthOperations serverZkAuthOperations; + private static final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); static { try { @@ -73,7 +85,7 @@ class CertificateControllerTest { ImmutableSet.of(AuthenticatedAccount.class, DisabledPermittedAuthenticatedAccount.class))) .setMapper(SystemMapper.getMapper()) .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) - .addResource(new CertificateController(certificateGenerator, serverZkAuthOperations)) + .addResource(new CertificateController(certificateGenerator, serverZkAuthOperations, clock)) .build(); @Test @@ -289,4 +301,97 @@ class CertificateControllerTest { assertThat(response.getStatus()).isEqualTo(401); } + + @Test + void testGetSingleGroupCredential() { + final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS); + + final GroupCredentials credentials = resources.getJerseyTest() + .target("/v1/certificate/auth/group") + .queryParam("redemptionStartSeconds", startOfDay.getEpochSecond()) + .queryParam("redemptionEndSeconds", startOfDay.getEpochSecond()) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(GroupCredentials.class); + + assertEquals(1, credentials.getCredentials().size()); + assertEquals(startOfDay.getEpochSecond(), credentials.getCredentials().get(0).getRedemptionTime()); + + final ClientZkAuthOperations clientZkAuthOperations = + new ClientZkAuthOperations(serverSecretParams.getPublicParams()); + + assertDoesNotThrow(() -> { + clientZkAuthOperations.receiveAuthCredentialWithPni( + AuthHelper.VALID_UUID, + AuthHelper.VALID_PNI, + (int) startOfDay.getEpochSecond(), + new AuthCredentialWithPniResponse(credentials.getCredentials().get(0).getCredential())); + }); + } + + @Test + void testGetWeekLongGroupCredentials() { + final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS); + + final GroupCredentials credentials = resources.getJerseyTest() + .target("/v1/certificate/auth/group") + .queryParam("redemptionStartSeconds", startOfDay.getEpochSecond()) + .queryParam("redemptionEndSeconds", startOfDay.plus(Duration.ofDays(7)).getEpochSecond()) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(GroupCredentials.class); + + assertEquals(8, credentials.getCredentials().size()); + + final ClientZkAuthOperations clientZkAuthOperations = + new ClientZkAuthOperations(serverSecretParams.getPublicParams()); + + for (int i = 0; i < 8; i++) { + final Instant redemptionTime = startOfDay.plus(Duration.ofDays(i)); + assertEquals(redemptionTime.getEpochSecond(), credentials.getCredentials().get(i).getRedemptionTime()); + + final int index = i; + + assertDoesNotThrow(() -> { + clientZkAuthOperations.receiveAuthCredentialWithPni( + AuthHelper.VALID_UUID, + AuthHelper.VALID_PNI, + redemptionTime.getEpochSecond(), + new AuthCredentialWithPniResponse(credentials.getCredentials().get(index).getCredential())); + }); + } + } + + @ParameterizedTest + @MethodSource + void testBadRedemptionTimes(final Instant redemptionStart, final Instant redemptionEnd) { + final Response response = resources.getJerseyTest() + .target("/v1/certificate/auth/group") + .queryParam("redemptionStartSeconds", redemptionStart.getEpochSecond()) + .queryParam("redemptionEndSeconds", redemptionEnd.getEpochSecond()) + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) + .get(); + + assertEquals(400, response.getStatus()); + } + + private static Stream testBadRedemptionTimes() { + return Stream.of( + // Start is after end + Arguments.of(clock.instant().plus(Duration.ofDays(1)), clock.instant()), + + // Start is in the past + Arguments.of(clock.instant().minus(Duration.ofDays(1)), clock.instant()), + + // End is too far in the future + Arguments.of(clock.instant(), clock.instant().plus(CertificateController.MAX_REDEMPTION_DURATION).plus(Duration.ofDays(1))), + + // Start is not at a day boundary + Arguments.of(clock.instant().plusSeconds(17), clock.instant().plus(Duration.ofDays(1))), + + // End is not at a day boundary + Arguments.of(clock.instant(), clock.instant().plusSeconds(17)) + ); + } }