diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java index 15d07000b..16a56050a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -434,18 +434,25 @@ public class ProfileController { final boolean isSelf, final ContainerRequestContext containerRequestContext) { - final ExpiringProfileKeyCredentialResponse expiringProfileKeyCredentialResponse = profilesManager.get(account.getUuid(), version) - .map(profile -> { - final ExpiringProfileKeyCredentialResponse profileKeyCredentialResponse; - try { - profileKeyCredentialResponse = ProfileHelper.getExpiringProfileKeyCredential(HexFormat.of().parseHex(encodedCredentialRequest), - profile, new ServiceId.Aci(account.getUuid()), zkProfileOperations); - } catch (VerificationFailedException | InvalidInputException e) { - throw new BadRequestException(e); - } - return profileKeyCredentialResponse; - }) - .orElse(null); + final ExpiringProfileKeyCredentialResponse expiringProfileKeyCredentialResponse; + + if (account.getCurrentProfileVersion().map(version::equals).orElse(false)) { + expiringProfileKeyCredentialResponse = profilesManager.get(account.getUuid(), version) + .map(profile -> { + final ExpiringProfileKeyCredentialResponse profileKeyCredentialResponse; + try { + profileKeyCredentialResponse = ProfileHelper.getExpiringProfileKeyCredential( + HexFormat.of().parseHex(encodedCredentialRequest), + profile, new ServiceId.Aci(account.getUuid()), zkProfileOperations); + } catch (VerificationFailedException | InvalidInputException e) { + throw new BadRequestException(e); + } + return profileKeyCredentialResponse; + }) + .orElse(null); + } else { + expiringProfileKeyCredentialResponse = null; + } return new ExpiringProfileKeyCredentialProfileResponse( buildVersionedProfileResponse(account, version, isSelf, true, containerRequestContext), diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java index 823a2709d..b0ca31a4a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/ExpiringProfileKeyCredentialProfileResponse.java @@ -19,7 +19,7 @@ public class ExpiringProfileKeyCredentialProfileResponse { @JsonUnwrapped private VersionedProfileResponse versionedProfileResponse; - @Schema(description = "Expiring profile key credential response. Null if profile version was not found") + @Schema(description = "Expiring profile key credential response. Null if profile version was not found or if the profile version is not current") @JsonProperty @JsonSerialize(using = ExpiringProfileKeyCredentialResponseAdapter.Serializing.class) @JsonDeserialize(using = ExpiringProfileKeyCredentialResponseAdapter.Deserializing.class) diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java index 54ba1c2ad..62efe4698 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java @@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.controllers; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.refEq; import static org.mockito.Mockito.any; @@ -51,7 +52,6 @@ import java.util.stream.Stream; import org.assertj.core.api.Condition; import org.glassfish.jersey.server.ServerProperties; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -176,7 +176,7 @@ class ProfileControllerTest { @BeforeEach void setup() { - reset(profilesManager); + reset(profilesManager, zkProfileOperations, accountsManager, rateLimiter); clock.pin(Instant.ofEpochSecond(42)); AccountsHelper.setupMockUpdate(accountsManager); @@ -241,12 +241,6 @@ class ProfileControllerTest { clearInvocations(zkProfileOperations); } - @AfterEach - void teardown() { - reset(accountsManager); - reset(rateLimiter); - } - @Test void testProfileGetByAci() throws RateLimitExceededException { final BaseProfileResponse profile = resources.getJerseyTest() @@ -1288,6 +1282,7 @@ class ProfileControllerTest { when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID); when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY)); when(account.isIdentifiedBy(new AciServiceIdentifier(AuthHelper.VALID_UUID))).thenReturn(true); + when(account.getCurrentProfileVersion()).thenReturn(Optional.of(version)); when(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(AuthHelper.VALID_UUID))).thenReturn(Optional.of(account)); when(profilesManager.get(AuthHelper.VALID_UUID, version)).thenReturn(Optional.of(versionedProfile)); @@ -1305,6 +1300,56 @@ class ProfileControllerTest { assertEquals(400, response.getStatus()); } + @Test + void testGetProfileWithExpiringProfileKeyCredentialNonCurrentVersion() + throws VerificationFailedException, InvalidInputException { + final String version = versionHex("version"); + + final ServerSecretParams serverSecretParams = ServerSecretParams.generate(); + final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams(); + + final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(serverPublicParams); + + final byte[] profileKeyBytes = TestRandomUtil.nextBytes(32); + + final ProfileKey profileKey = new ProfileKey(profileKeyBytes); + final ProfileKeyCommitment profileKeyCommitment = profileKey.getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); + + final VersionedProfile versionedProfile = mock(VersionedProfile.class); + when(versionedProfile.commitment()).thenReturn(profileKeyCommitment.serialize()); + final String avatar = "avatar"; + when(versionedProfile.avatar()).thenReturn(avatar); + + final ProfileKeyCredentialRequestContext profileKeyCredentialRequestContext = + clientZkProfile.createProfileKeyCredentialRequestContext(new ServiceId.Aci(AuthHelper.VALID_UUID), profileKey); + + final ProfileKeyCredentialRequest credentialRequest = profileKeyCredentialRequestContext.getRequest(); + + final Account account = mock(Account.class); + when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID); + when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY)); + when(account.isIdentifiedBy(new AciServiceIdentifier(AuthHelper.VALID_UUID))).thenReturn(true); + when(account.getCurrentProfileVersion()).thenReturn(Optional.of(versionHex("the-current-version"))); + + when(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(AuthHelper.VALID_UUID))).thenReturn(Optional.of(account)); + when(profilesManager.get(AuthHelper.VALID_UUID, version)).thenReturn(Optional.of(versionedProfile)); + + final Response response = resources.getJerseyTest() + .target(String.format("/v1/profile/%s/%s/%s", AuthHelper.VALID_UUID, version, + HexFormat.of().formatHex(credentialRequest.serialize()))) + .queryParam("credentialType", "expiringProfileKey") + .request() + .headers(new MultivaluedHashMap<>(Map.of(HeaderUtils.UNIDENTIFIED_ACCESS_KEY, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_KEY)))) + .get(); + + assertEquals(200, response.getStatus()); + final ExpiringProfileKeyCredentialProfileResponse expiringProfileKeyCredentialProfileResponse = response.readEntity( + ExpiringProfileKeyCredentialProfileResponse.class); + + assertNull(expiringProfileKeyCredentialProfileResponse.getCredential()); + assertEquals(avatar, expiringProfileKeyCredentialProfileResponse.getVersionedProfileResponse().avatar()); + } + @Test void testSetProfileBadgesMissingFromRequest() throws InvalidInputException { final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID));