diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyController.java index 8b4bb4a0e..d8e931a14 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyController.java @@ -167,19 +167,19 @@ public class KeyTransparencyController { try { final List monitorKeys = new ArrayList<>(List.of( - createMonitorKey(getFullSearchKeyByteString(ACI_PREFIX, request.aci().toCompactByteArray()), - request.aciPositions()) + createMonitorKey(getFullSearchKeyByteString(ACI_PREFIX, request.aci().value().toCompactByteArray()), + request.aci().positions()) )); request.usernameHash().ifPresent(usernameHash -> - monitorKeys.add(createMonitorKey(getFullSearchKeyByteString(USERNAME_PREFIX, usernameHash), - request.usernameHashPositions().get())) + monitorKeys.add(createMonitorKey(getFullSearchKeyByteString(USERNAME_PREFIX, usernameHash.value()), + usernameHash.positions())) ); request.e164().ifPresent(e164 -> monitorKeys.add( - createMonitorKey(getFullSearchKeyByteString(E164_PREFIX, e164.getBytes(StandardCharsets.UTF_8)), - request.e164Positions().get())) + createMonitorKey(getFullSearchKeyByteString(E164_PREFIX, e164.value().getBytes(StandardCharsets.UTF_8)), + e164.positions())) ); return new KeyTransparencyMonitorResponse(keyTransparencyServiceClient.monitor( diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencyMonitorRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencyMonitorRequest.java index a43ba2805..eaed0eaae 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencyMonitorRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/KeyTransparencyMonitorRequest.java @@ -8,58 +8,78 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.Optional; +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Positive; import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter; import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter; -import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Positive; -import java.util.List; -import java.util.Optional; - public record KeyTransparencyMonitorRequest( + + @Valid @NotNull - @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) - @JsonDeserialize(using = ServiceIdentifierAdapter.AciServiceIdentifierDeserializer.class) - @Schema(description = "The aci identifier to monitor") - AciServiceIdentifier aci, + AciMonitor aci, - @NotEmpty - @Schema(description = "A list of log tree positions maintained by the client for the aci search key.") - List<@Positive Long> aciPositions, + @Valid + @NotNull + Optional<@Valid E164Monitor> e164, - @Schema(description = "The e164-formatted phone number to monitor") - Optional e164, - - @Schema(description = "A list of log tree positions maintained by the client for the e164 search key.") - Optional> e164Positions, - - @JsonSerialize(contentUsing = ByteArrayBase64UrlAdapter.Serializing.class) - @JsonDeserialize(contentUsing = ByteArrayBase64UrlAdapter.Deserializing.class) - @Schema(description = "The username hash to monitor, encoded in url-safe unpadded base64.") - Optional usernameHash, - - @Schema(description = "A list of log tree positions maintained by the client for the username hash search key.") - Optional> usernameHashPositions, + @Valid + @NotNull + Optional<@Valid UsernameHashMonitor> usernameHash, @Schema(description = "The tree head size to prove consistency against.") + @NotNull Optional<@Positive Long> lastNonDistinguishedTreeHeadSize, @Schema(description = "The distinguished tree head size to prove consistency against.") + @NotNull Optional<@Positive Long> lastDistinguishedTreeHeadSize ) { - @AssertTrue - public boolean isUsernameHashFieldsValid() { - return (usernameHash.isEmpty() && usernameHashPositions.isEmpty()) || - (usernameHash.isPresent() && usernameHashPositions.isPresent() && !usernameHashPositions.get().isEmpty()); - } + public record AciMonitor( + @NotNull + @JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class) + @JsonDeserialize(using = ServiceIdentifierAdapter.AciServiceIdentifierDeserializer.class) + @Schema(description = "The aci identifier to monitor") + AciServiceIdentifier value, - @AssertTrue - public boolean isE164VFieldsValid() { - return (e164.isEmpty() && e164Positions.isEmpty()) || - (e164.isPresent() && e164Positions.isPresent() && !e164Positions.get().isEmpty()); - } + @Schema(description = "A list of log tree positions maintained by the client for the aci search key.") + @Valid + @NotNull + @NotEmpty + List<@Positive Long> positions + ) {} + + public record E164Monitor( + @Schema(description = "The e164-formatted phone number to monitor") + @NotBlank + String value, + + @Schema(description = "A list of log tree positions maintained by the client for the e164 search key.") + @NotNull + @NotEmpty + @Valid + List<@Positive Long> positions + ) {} + + public record UsernameHashMonitor( + + @Schema(description = "The username hash to monitor, encoded in url-safe unpadded base64.") + @JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class) + @NotNull + @NotEmpty + byte[] value, + + @Schema(description = "A list of log tree positions maintained by the client for the username hash search key.") + @NotNull + @NotEmpty + @Valid List<@Positive Long> positions + ) {} } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyControllerTest.java index d1e411c3f..9e79d4409 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/KeyTransparencyControllerTest.java @@ -12,6 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -129,7 +130,7 @@ public class KeyTransparencyControllerTest { System.arraycopy(charBytes, 0, expectedFullSearchKey, 0, charBytes.length); System.arraycopy(aci, 0, expectedFullSearchKey, charBytes.length, aci.length); - assertArrayEquals(expectedFullSearchKey, getFullSearchKeyByteString(KeyTransparencyController.ACI_PREFIX, aci).toByteArray()); + assertArrayEquals(expectedFullSearchKey, getFullSearchKeyByteString(ACI_PREFIX, aci).toByteArray()); } @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @@ -527,8 +528,20 @@ public class KeyTransparencyControllerTest { final Optional> e164Positions, final Optional lastTreeHeadSize, final Optional distinguishedTreeHeadSize) { - final KeyTransparencyMonitorRequest request = new KeyTransparencyMonitorRequest(aci, aciPositions, - e164, e164Positions, usernameHash, usernameHashPositions, lastTreeHeadSize, distinguishedTreeHeadSize); + + final Optional e164Monitor = e164.map( + value -> new KeyTransparencyMonitorRequest.E164Monitor(value, e164Positions.orElse(Collections.emptyList()))) + .or(() -> e164Positions.map(positions -> new KeyTransparencyMonitorRequest.E164Monitor(null, positions))); + + final Optional usernameHashMonitor = usernameHash.map( + value -> new KeyTransparencyMonitorRequest.UsernameHashMonitor(value, + usernameHashPositions.orElse(Collections.emptyList()))) + .or(() -> usernameHashPositions.map( + positions -> new KeyTransparencyMonitorRequest.UsernameHashMonitor(null, positions))); + + final KeyTransparencyMonitorRequest request = new KeyTransparencyMonitorRequest( + new KeyTransparencyMonitorRequest.AciMonitor(aci, aciPositions), e164Monitor, usernameHashMonitor, + lastTreeHeadSize, distinguishedTreeHeadSize); try { return SystemMapper.jsonMapper().writeValueAsString(request); } catch (final JsonProcessingException e) {