Move /v1/svrb/auth to /v1/archives/auth/svrb

This commit is contained in:
ravi-signal
2025-08-01 12:00:44 -05:00
committed by GitHub
parent f8d27d8fab
commit e8a1854c5e
23 changed files with 243 additions and 189 deletions

View File

@@ -20,6 +20,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
@@ -36,6 +37,7 @@ import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -43,11 +45,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.annotation.Nullable;
import org.apache.commons.lang3.RandomStringUtils;
@@ -71,9 +70,13 @@ import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
import org.signal.libsignal.zkgroup.backups.BackupLevel;
import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecoveryClient;
import org.whispersystems.textsecuregcm.storage.DynamoDbExtension;
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema;
import org.whispersystems.textsecuregcm.util.AttributeValues;
@@ -107,6 +110,18 @@ public class BackupManagerTest {
private final byte[] backupKey = TestRandomUtil.nextBytes(32);
private final UUID aci = UUID.randomUUID();
private static final SecureValueRecoveryConfiguration CFG = new SecureValueRecoveryConfiguration(
"",
randomSecretBytes(32),
randomSecretBytes(32),
null,
null,
null);
private final ExternalServiceCredentialsGenerator svrbCredentialGenerator =
SecureValueRecoveryBCredentialsGeneratorFactory.svrbCredentialsGenerator(CFG, testClock);
private final SecureValueRecoveryClient svrbClient = mock(SecureValueRecoveryClient.class);
private BackupManager backupManager;
private BackupsDb backupsDb;
@@ -131,6 +146,8 @@ public class BackupManagerTest {
tusAttachmentGenerator,
tusCredentialGenerator,
remoteStorageManager,
svrbCredentialGenerator,
svrbClient,
testClock);
}
@@ -697,9 +714,13 @@ public class BackupManagerTest {
testClock.pin(Instant.ofEpochSecond(10));
when(svrbClient.removeData(anyString())).thenReturn(CompletableFuture.completedFuture(null));
// Deleting should swap the backupDir for the user
backupManager.deleteEntireBackup(original).join();
verifyNoInteractions(remoteStorageManager);
verify(svrbClient).removeData(HexFormat.of().formatHex(BackupsDb.hashedBackupId(original.backupId())));
final AuthenticatedBackupUser after = retrieveBackupUser(original.backupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID);
assertThat(original.backupDir()).isNotEqualTo(after.backupDir());
assertThat(original.mediaDir()).isNotEqualTo(after.mediaDir());
@@ -959,11 +980,15 @@ public class BackupManagerTest {
new RemoteStorageManager.ListResult.Entry("ghi", 1)), Optional.empty())));
when(remoteStorageManager.delete(anyString())).thenReturn(CompletableFuture.completedFuture(1L));
when(svrbClient.removeData(anyString())).thenReturn(CompletableFuture.completedFuture(null));
backupManager.expireBackup(expiredBackup(expirationType, backupUser)).join();
verify(remoteStorageManager, times(1)).list(anyString(), any(), anyLong());
verify(remoteStorageManager, times(1)).delete(expectedPrefixToDelete + "abc");
verify(remoteStorageManager, times(1)).delete(expectedPrefixToDelete + "def");
verify(remoteStorageManager, times(1)).delete(expectedPrefixToDelete + "ghi");
verify(svrbClient, times(expirationType == ExpiredBackup.ExpirationType.ALL ? 1 : 0))
.removeData(HexFormat.of().formatHex(BackupsDb.hashedBackupId(backupUser.backupId())));
verifyNoMoreInteractions(remoteStorageManager);
final BackupsDb.TimestampedUsageInfo usage = backupsDb.getMediaUsage(backupUser).join();
@@ -1020,6 +1045,32 @@ public class BackupManagerTest {
verifyNoMoreInteractions(remoteStorageManager);
}
@ParameterizedTest
@EnumSource(BackupLevel.class)
void svrbAuthValid(BackupLevel backupLevel) {
testClock.pin(Instant.ofEpochSecond(123));
final AuthenticatedBackupUser backupUser =
backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, backupLevel);
final ExternalServiceCredentials creds = backupManager.generateSvrbAuth(backupUser);
assertThat(HexFormat.of().parseHex(creds.username())).hasSize(16);
final String[] split = creds.password().split(":", 2);
assertThat(Long.parseLong(split[0])).isEqualTo(123);
}
@ParameterizedTest
@EnumSource(BackupLevel.class)
void svrbAuthInvalid(BackupLevel backupLevel) {
// Can't use MEDIA for svrb auth
final AuthenticatedBackupUser backupUser =
backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, backupLevel);
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() -> backupManager.generateSvrbAuth(backupUser))
.extracting(StatusRuntimeException::getStatus)
.extracting(Status::getCode)
.isEqualTo(Status.Code.UNAUTHENTICATED);
}
private CopyResult copyError(final AuthenticatedBackupUser backupUser, Throwable copyException) {
when(tusCredentialGenerator.generateUpload(any()))
.thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));

View File

@@ -63,6 +63,7 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
import org.whispersystems.textsecuregcm.backup.BackupAuthTestUtil;
import org.whispersystems.textsecuregcm.backup.BackupManager;
@@ -116,6 +117,7 @@ public class ArchiveControllerTest {
@ParameterizedTest
@CsvSource(textBlock = """
GET, v1/archives/auth/read,
GET, v1/archives/auth/svrb,
GET, v1/archives/,
GET, v1/archives/upload/form,
GET, v1/archives/media/upload/form,
@@ -663,6 +665,24 @@ public class ArchiveControllerTest {
assertThat(response.headers()).containsExactlyEntriesOf(Map.of("key", "value"));
}
@Test
public void svrbAuth() throws VerificationFailedException {
final BackupAuthCredentialPresentation presentation =
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
when(backupManager.authenticateBackupUser(any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
final ExternalServiceCredentials credentials = new ExternalServiceCredentials("username", "password");
when(backupManager.generateSvrbAuth(any())).thenReturn(credentials);
final ExternalServiceCredentials response = resources.getJerseyTest()
.target("v1/archives/auth/svrb")
.request()
.header("X-Signal-ZK-Auth", Base64.getEncoder().encodeToString(presentation.serialize()))
.header("X-Signal-ZK-Auth-Signature", "aaa")
.get(ExternalServiceCredentials.class);
assertThat(response).isEqualTo(credentials);
}
@Test
public void readAuthInvalidParam() throws VerificationFailedException {
final BackupAuthCredentialPresentation presentation =

View File

@@ -1,70 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.controllers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
import io.dropwizard.testing.junit5.ResourceExtension;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.MutableClock;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.time.Instant;
import java.util.HexFormat;
@ExtendWith(DropwizardExtensionsSupport.class)
public class SecureValueRecoveryBControllerTest {
private static final SecureValueRecoveryConfiguration CFG = new SecureValueRecoveryConfiguration(
"",
randomSecretBytes(32),
randomSecretBytes(32),
null,
null,
null
);
private static final MutableClock CLOCK = new MutableClock();
private static final ExternalServiceCredentialsGenerator CREDENTIAL_GENERATOR =
SecureValueRecoveryBController.credentialsGenerator(CFG, CLOCK);
private static final SecureValueRecoveryBController CONTROLLER =
new SecureValueRecoveryBController(CREDENTIAL_GENERATOR);
private static final ResourceExtension RESOURCES = ResourceExtension.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
.setMapper(SystemMapper.jsonMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(CONTROLLER)
.build();
@Test
public void testGetCredentials() {
CLOCK.setTimeInstant(Instant.ofEpochSecond(123));
final ExternalServiceCredentials creds = RESOURCES.getJerseyTest()
.target("/v1/svrb/auth")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.get(ExternalServiceCredentials.class);
assertThat(HexFormat.of().parseHex(creds.username())).hasSize(16);
System.out.println(creds.password());
final String[] split = creds.password().split(":", 2);
assertThat(Long.parseLong(split[0])).isEqualTo(123);
}
}

View File

@@ -135,7 +135,7 @@ class SecureValueRecoveryClientTest {
final String username = RandomStringUtils.secure().nextAlphabetic(16);
final String password = RandomStringUtils.secure().nextAlphanumeric(32);
when(credentialsGenerator.generateForUuid(accountUuid)).thenReturn(
when(credentialsGenerator.generateFor(accountUuid.toString())).thenReturn(
new ExternalServiceCredentials(username, password));
wireMock.stubFor(delete(urlEqualTo(SecureValueRecoveryClient.DELETE_PATH))

View File

@@ -137,7 +137,7 @@ public class AccountCreationDeletionIntegrationTest {
when(secureStorageClient.deleteStoredData(any())).thenReturn(CompletableFuture.completedFuture(null));
final SecureValueRecoveryClient svr2Client = mock(SecureValueRecoveryClient.class);
when(svr2Client.removeData(any())).thenReturn(CompletableFuture.completedFuture(null));
when(svr2Client.removeData(any(UUID.class))).thenReturn(CompletableFuture.completedFuture(null));
final PhoneNumberIdentifiers phoneNumberIdentifiers =
new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
@@ -169,7 +169,6 @@ public class AccountCreationDeletionIntegrationTest {
profilesManager,
secureStorageClient,
svr2Client,
svr2Client,
disconnectionRequestManager,
registrationRecoveryPasswordsManager,
clientPublicKeysManager,

View File

@@ -128,7 +128,7 @@ class AccountsManagerChangeNumberIntegrationTest {
when(secureStorageClient.deleteStoredData(any())).thenReturn(CompletableFuture.completedFuture(null));
final SecureValueRecoveryClient svr2Client = mock(SecureValueRecoveryClient.class);
when(svr2Client.removeData(any())).thenReturn(CompletableFuture.completedFuture(null));
when(svr2Client.removeData(any(UUID.class))).thenReturn(CompletableFuture.completedFuture(null));
disconnectionRequestManager = mock(DisconnectionRequestManager.class);
@@ -158,7 +158,6 @@ class AccountsManagerChangeNumberIntegrationTest {
profilesManager,
secureStorageClient,
svr2Client,
svr2Client,
disconnectionRequestManager,
registrationRecoveryPasswordsManager,
clientPublicKeysManager,

View File

@@ -135,7 +135,6 @@ class AccountsManagerConcurrentModificationIntegrationTest {
mock(ProfilesManager.class),
mock(SecureStorageClient.class),
mock(SecureValueRecoveryClient.class),
mock(SecureValueRecoveryClient.class),
mock(DisconnectionRequestManager.class),
mock(RegistrationRecoveryPasswordsManager.class),
mock(ClientPublicKeysManager.class),

View File

@@ -73,7 +73,6 @@ public class AccountsManagerDeviceTransferIntegrationTest {
mock(ProfilesManager.class),
mock(SecureStorageClient.class),
mock(SecureValueRecoveryClient.class),
mock(SecureValueRecoveryClient.class),
mock(DisconnectionRequestManager.class),
mock(RegistrationRecoveryPasswordsManager.class),
mock(ClientPublicKeysManager.class),

View File

@@ -134,7 +134,6 @@ class AccountsManagerTest {
private RedisAdvancedClusterAsyncCommands<String, String> asyncClusterCommands;
private AccountsManager accountsManager;
private SecureValueRecoveryClient svr2Client;
private SecureValueRecoveryClient svrbClient;
private DynamicConfiguration dynamicConfiguration;
private static final Answer<?> ACCOUNT_UPDATE_ANSWER = (answer) -> {
@@ -193,13 +192,10 @@ class AccountsManagerTest {
}).when(accounts).changeNumber(any(), anyString(), any(), any(), any());
final SecureStorageClient storageClient = mock(SecureStorageClient.class);
when(storageClient.deleteStoredData(any())).thenReturn(CompletableFuture.completedFuture(null));
when(storageClient.deleteStoredData(any(UUID.class))).thenReturn(CompletableFuture.completedFuture(null));
svr2Client = mock(SecureValueRecoveryClient.class);
when(svr2Client.removeData(any())).thenReturn(CompletableFuture.completedFuture(null));
svrbClient = mock(SecureValueRecoveryClient.class);
when(svrbClient.removeData(any())).thenReturn(CompletableFuture.completedFuture(null));
when(svr2Client.removeData(any(UUID.class))).thenReturn(CompletableFuture.completedFuture(null));
final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
phoneNumberIdentifiersByE164 = new HashMap<>();
@@ -259,7 +255,6 @@ class AccountsManagerTest {
profilesManager,
storageClient,
svr2Client,
svrbClient,
disconnectionRequestManager,
registrationRecoveryPasswordsManager,
clientPublicKeysManager,

View File

@@ -162,7 +162,6 @@ class AccountsManagerUsernameIntegrationTest {
profileManager,
mock(SecureStorageClient.class),
mock(SecureValueRecoveryClient.class),
mock(SecureValueRecoveryClient.class),
disconnectionRequestManager,
mock(RegistrationRecoveryPasswordsManager.class),
mock(ClientPublicKeysManager.class),

View File

@@ -136,7 +136,7 @@ public class AddRemoveDeviceIntegrationTest {
when(secureStorageClient.deleteStoredData(any())).thenReturn(CompletableFuture.completedFuture(null));
final SecureValueRecoveryClient svr2Client = mock(SecureValueRecoveryClient.class);
when(svr2Client.removeData(any())).thenReturn(CompletableFuture.completedFuture(null));
when(svr2Client.removeData(any(UUID.class))).thenReturn(CompletableFuture.completedFuture(null));
final PhoneNumberIdentifiers phoneNumberIdentifiers =
new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
@@ -170,7 +170,6 @@ public class AddRemoveDeviceIntegrationTest {
profilesManager,
secureStorageClient,
svr2Client,
svr2Client,
mock(DisconnectionRequestManager.class),
mock(RegistrationRecoveryPasswordsManager.class),
clientPublicKeysManager,