From 760c5737f98b929acc7a91257fd543945b389ce8 Mon Sep 17 00:00:00 2001 From: Ravi Khadiwala Date: Mon, 3 Feb 2025 18:26:58 -0600 Subject: [PATCH] Add field to RestoreAccountRequest for device transfer initialization --- .../entities/RestoreAccountRequest.java | 14 +++++++- .../controllers/DeviceControllerTest.java | 33 ++++++++++++++++--- ...sManagerDeviceTransferIntegrationTest.java | 13 ++++++-- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RestoreAccountRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RestoreAccountRequest.java index 05351a503..6f041fd9f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RestoreAccountRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RestoreAccountRequest.java @@ -5,8 +5,13 @@ 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 jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.util.ByteArrayAdapter; @Schema(description = """ Represents a request from a new device to restore account data by some method. @@ -14,7 +19,14 @@ import jakarta.validation.constraints.NotNull; public record RestoreAccountRequest( @NotNull @Schema(description = "The method by which the new device has requested account data restoration") - Method method) { + Method method, + + @Schema(description = "Additional data to use to bootstrap a connection between devices, in standard unpadded base64.", + implementation = String.class) + @JsonSerialize(using = ByteArrayAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) + @Size(max = 4096) + @Nullable byte[] deviceTransferBootstrap) { public enum Method { @Schema(description = "Restore account data from a remote message history backup") diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java index 0cac6fce1..7abb8b42f 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java @@ -42,6 +42,7 @@ import java.util.concurrent.CompletableFuture; import java.util.stream.IntStream; import java.util.stream.Stream; import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; import org.glassfish.jersey.server.ServerProperties; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.junit.jupiter.api.AfterEach; @@ -51,6 +52,7 @@ import org.junit.jupiter.api.extension.ExtendWith; 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.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; @@ -1267,7 +1269,7 @@ class DeviceControllerTest { void recordRestoreAccountRequest() { final String token = RandomStringUtils.secure().nextAlphanumeric(16); final RestoreAccountRequest restoreAccountRequest = - new RestoreAccountRequest(RestoreAccountRequest.Method.LOCAL_BACKUP); + new RestoreAccountRequest(RestoreAccountRequest.Method.LOCAL_BACKUP, null); when(accountsManager.recordRestoreAccountRequest(token, restoreAccountRequest)) .thenReturn(CompletableFuture.completedFuture(null)); @@ -1285,7 +1287,7 @@ class DeviceControllerTest { void recordRestoreAccountRequestBadToken() { final String token = RandomStringUtils.secure().nextAlphanumeric(128); final RestoreAccountRequest restoreAccountRequest = - new RestoreAccountRequest(RestoreAccountRequest.Method.LOCAL_BACKUP); + new RestoreAccountRequest(RestoreAccountRequest.Method.LOCAL_BACKUP, null); try (final Response response = resources.getJerseyTest() .target("/v1/devices/restore_account/" + token) @@ -1299,7 +1301,7 @@ class DeviceControllerTest { @Test void recordRestoreAccountRequestInvalidRequest() { final String token = RandomStringUtils.secure().nextAlphanumeric(16); - final RestoreAccountRequest restoreAccountRequest = new RestoreAccountRequest(null); + final RestoreAccountRequest restoreAccountRequest = new RestoreAccountRequest(null, null); try (final Response response = resources.getJerseyTest() .target("/v1/devices/restore_account/" + token) @@ -1310,11 +1312,34 @@ class DeviceControllerTest { } } + @ParameterizedTest + @CsvSource({ + "0, true", + "4096, true", + "4097, false" + }) + void recordRestoreAccountRequestBootstrapLengthLimit(int bootstrapLength, boolean valid) { + final String token = RandomStringUtils.secure().nextAlphanumeric(16); + + final byte[] bootstrap = TestRandomUtil.nextBytes(bootstrapLength); + final RestoreAccountRequest restoreAccountRequest = new RestoreAccountRequest( + RestoreAccountRequest.Method.DEVICE_TRANSFER, bootstrap); + + try (final Response response = resources.getJerseyTest() + .target("/v1/devices/restore_account/" + token) + .request() + .put(Entity.json(restoreAccountRequest))) { + + assertEquals(valid ? 204 : 422, response.getStatus()); + } + + } + @Test void waitForDeviceTransferRequest() { final String token = RandomStringUtils.secure().nextAlphanumeric(16); final RestoreAccountRequest restoreAccountRequest = - new RestoreAccountRequest(RestoreAccountRequest.Method.LOCAL_BACKUP); + new RestoreAccountRequest(RestoreAccountRequest.Method.LOCAL_BACKUP, null); when(accountsManager.waitForRestoreAccountRequest(eq(token), any())) .thenReturn(CompletableFuture.completedFuture(Optional.of(restoreAccountRequest))); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerDeviceTransferIntegrationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerDeviceTransferIntegrationTest.java index bd360e103..86a9b407b 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerDeviceTransferIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerDeviceTransferIntegrationTest.java @@ -21,6 +21,7 @@ import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClusterClient; import org.whispersystems.textsecuregcm.redis.RedisServerExtension; import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient; import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecovery2Client; +import org.whispersystems.textsecuregcm.util.TestRandomUtil; import java.nio.charset.StandardCharsets; import java.time.Clock; @@ -33,7 +34,9 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -178,8 +181,9 @@ public class AccountsManagerDeviceTransferIntegrationTest { @Test void waitForRestoreAccountRequest() { final String token = RandomStringUtils.secure().nextAlphanumeric(16); + final byte[] deviceTransferBootstrap = TestRandomUtil.nextBytes(100); final RestoreAccountRequest restoreAccountRequest = - new RestoreAccountRequest(RestoreAccountRequest.Method.DEVICE_TRANSFER); + new RestoreAccountRequest(RestoreAccountRequest.Method.DEVICE_TRANSFER, deviceTransferBootstrap); final CompletableFuture> displacedFuture = accountsManager.waitForRestoreAccountRequest(token, Duration.ofSeconds(5)); @@ -191,14 +195,17 @@ public class AccountsManagerDeviceTransferIntegrationTest { accountsManager.recordRestoreAccountRequest(token, restoreAccountRequest).join(); - assertEquals(Optional.of(restoreAccountRequest), activeFuture.join()); + final Optional result = activeFuture.join(); + assertTrue(result.isPresent()); + assertEquals(restoreAccountRequest.method(), result.get().method()); + assertArrayEquals(restoreAccountRequest.deviceTransferBootstrap(), result.get().deviceTransferBootstrap()); } @Test void waitForRestoreAccountRequestAlreadyRequested() { final String token = RandomStringUtils.secure().nextAlphanumeric(16); final RestoreAccountRequest restoreAccountRequest = - new RestoreAccountRequest(RestoreAccountRequest.Method.DEVICE_TRANSFER); + new RestoreAccountRequest(RestoreAccountRequest.Method.DEVICE_TRANSFER, null); accountsManager.recordRestoreAccountRequest(token, restoreAccountRequest).join();