diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 523f145fc..c2b1f44c8 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -93,6 +93,7 @@ import org.whispersystems.textsecuregcm.backup.BackupManager; import org.whispersystems.textsecuregcm.backup.BackupsDb; import org.whispersystems.textsecuregcm.backup.Cdn3BackupCredentialGenerator; import org.whispersystems.textsecuregcm.backup.Cdn3RemoteStorageManager; +import org.whispersystems.textsecuregcm.backup.SecureValueRecoveryBCredentialsGeneratorFactory; import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter; import org.whispersystems.textsecuregcm.captcha.CaptchaChecker; import org.whispersystems.textsecuregcm.captcha.CaptchaClient; @@ -126,7 +127,6 @@ import org.whispersystems.textsecuregcm.controllers.RemoteConfigController; import org.whispersystems.textsecuregcm.controllers.RemoteConfigControllerV1; import org.whispersystems.textsecuregcm.controllers.SecureStorageController; import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller; -import org.whispersystems.textsecuregcm.controllers.SecureValueRecoveryBController; import org.whispersystems.textsecuregcm.controllers.StickerController; import org.whispersystems.textsecuregcm.controllers.SubscriptionController; import org.whispersystems.textsecuregcm.controllers.VerificationController; @@ -595,9 +595,9 @@ public class WhisperServerService extends Application deleteEntireBackup(final AuthenticatedBackupUser backupUser) { checkBackupLevel(backupUser, BackupLevel.FREE); - return backupsDb + + // Clients only include SVRB data with their messages backup-id + final CompletableFuture svrbRemoval = switch(backupUser.credentialType()) { + case BackupCredentialType.MESSAGES -> secureValueRecoveryBClient.removeData(svrbIdentifier(backupUser)); + case BackupCredentialType.MEDIA -> CompletableFuture.completedFuture(null); + }; + return svrbRemoval.thenCompose(_ -> backupsDb // Try to swap out the backupDir for the user .scheduleBackupDeletion(backupUser) // If there was already a pending swap, try to delete the cdn objects directly .exceptionallyCompose(ExceptionUtils.exceptionallyHandler(BackupsDb.PendingDeletionException.class, e -> AsyncTimerUtil.record(SYNCHRONOUS_DELETE_TIMER, () -> - deletePrefix(backupUser.backupDir(), DELETION_CONCURRENCY)))); + deletePrefix(backupUser.backupDir(), DELETION_CONCURRENCY))))); } @@ -617,12 +651,17 @@ public class BackupManager { * @return A stage that completes when the deletion operation is finished */ public CompletableFuture expireBackup(final ExpiredBackup expiredBackup) { - return backupsDb.startExpiration(expiredBackup) + // Clients only include SVRB data with their messages backup-id + final CompletableFuture svrbRemoval = switch(expiredBackup.expirationType()) { + case ALL -> secureValueRecoveryBClient.removeData(svrbIdentifier(expiredBackup.hashedBackupId())); + case MEDIA, GARBAGE_COLLECTION -> CompletableFuture.completedFuture(null); + }; + return svrbRemoval.thenCompose(_ -> backupsDb.startExpiration(expiredBackup) // the deletion operation is effectively single threaded -- it's expected that the caller can increase // concurrency by deleting more backups at once, rather than increasing concurrency deleting an individual // backup .thenCompose(ignored -> deletePrefix(expiredBackup.prefixToDelete(), 1)) - .thenCompose(ignored -> backupsDb.finishExpiration(expiredBackup)); + .thenCompose(ignored -> backupsDb.finishExpiration(expiredBackup))); } /** diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/SecureValueRecoveryBCredentialsGeneratorFactory.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/SecureValueRecoveryBCredentialsGeneratorFactory.java new file mode 100644 index 000000000..29c438dd1 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/SecureValueRecoveryBCredentialsGeneratorFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.backup; + +import com.google.common.annotations.VisibleForTesting; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; +import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration; +import java.time.Clock; + +public class SecureValueRecoveryBCredentialsGeneratorFactory { + private SecureValueRecoveryBCredentialsGeneratorFactory() {} + + + @VisibleForTesting + static ExternalServiceCredentialsGenerator svrbCredentialsGenerator( + final SecureValueRecoveryConfiguration cfg, + final Clock clock) { + return ExternalServiceCredentialsGenerator + .builder(cfg.userAuthenticationTokenSharedSecret()) + .withUserDerivationKey(cfg.userIdTokenSharedSecret().value()) + .prependUsername(false) + .withDerivedUsernameTruncateLength(16) + .withClock(clock) + .build(); + } + + public static ExternalServiceCredentialsGenerator svrbCredentialsGenerator(final SecureValueRecoveryConfiguration cfg) { + return svrbCredentialsGenerator(cfg, Clock.systemUTC()); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java index 67cd0989f..a25323b21 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java @@ -64,6 +64,7 @@ import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest; import org.signal.libsignal.zkgroup.backups.BackupCredentialType; import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice; +import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; import org.whispersystems.textsecuregcm.backup.BackupAuthManager; import org.whispersystems.textsecuregcm.backup.BackupManager; import org.whispersystems.textsecuregcm.backup.CopyParameters; @@ -389,6 +390,35 @@ public class ArchiveController { .thenApply(ReadAuthResponse::new); } + @GET + @Path("/auth/svrb") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Generate credentials for SVRB", + description = """ + Generate SVRB service credentials. Generated credentials have an expiration time of 1 day (subject to change) + """) + @ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true) + @ApiResponseZkAuth + public CompletionStage svrbAuth( + @Auth final Optional account, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, + + @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class)) + @NotNull + @HeaderParam(X_SIGNAL_ZK_AUTH) final ArchiveController.BackupAuthCredentialPresentationHeader presentation, + + @Parameter(description = BackupAuthCredentialPresentationSignature.DESCRIPTION, schema = @Schema(implementation = String.class)) + @NotNull + @HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature) { + if (account.isPresent()) { + throw new BadRequestException("must not use authenticated connection for anonymous operations"); + } + return backupManager + .authenticateBackupUser(presentation.presentation, signature.signature, userAgent) + .thenApply(backupManager::generateSvrbAuth); + } + public record BackupInfoResponse( @Schema(description = "The CDN type where the message backup is stored. Media may be stored elsewhere.") int cdn, diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryBController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryBController.java deleted file mode 100644 index d3612e01b..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryBController.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.controllers; - -import com.google.common.annotations.VisibleForTesting; -import io.dropwizard.auth.Auth; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import java.time.Clock; -import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice; -import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; -import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; -import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration; - -@Path("/v1/svrb") -@Tag(name = "Secure Value Recovery B") -public class SecureValueRecoveryBController { - - public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecoveryConfiguration cfg) { - return credentialsGenerator(cfg, Clock.systemUTC()); - } - - @VisibleForTesting - public static ExternalServiceCredentialsGenerator credentialsGenerator(final SecureValueRecoveryConfiguration cfg, - final Clock clock) { - return ExternalServiceCredentialsGenerator - .builder(cfg.userAuthenticationTokenSharedSecret()) - .withUserDerivationKey(cfg.userIdTokenSharedSecret().value()) - .prependUsername(false) - .withDerivedUsernameTruncateLength(16) - .withClock(clock) - .build(); - } - - private final ExternalServiceCredentialsGenerator svrbCredentialGenerator; - - public SecureValueRecoveryBController(final ExternalServiceCredentialsGenerator svrbCredentialGenerator) { - this.svrbCredentialGenerator = svrbCredentialGenerator; - } - - @GET - @Path("/auth") - @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Generate credentials for SVRB", - description = """ - Generate SVRB service credentials. Generated credentials have an expiration time of 1 day (subject to change) - """ - ) - @ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true) - @ApiResponse(responseCode = "401", description = "Account authentication check failed.") - public ExternalServiceCredentials getAuth(@Auth final AuthenticatedDevice auth) { - return svrbCredentialGenerator.generateFor(auth.accountIdentifier().toString()); - } -} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java index 090c41e8d..3dd2ee8e6 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java @@ -21,6 +21,8 @@ import org.signal.chat.backup.GetBackupInfoRequest; import org.signal.chat.backup.GetBackupInfoResponse; import org.signal.chat.backup.GetCdnCredentialsRequest; import org.signal.chat.backup.GetCdnCredentialsResponse; +import org.signal.chat.backup.GetSvrBCredentialsRequest; +import org.signal.chat.backup.GetSvrBCredentialsResponse; import org.signal.chat.backup.GetUploadFormRequest; import org.signal.chat.backup.GetUploadFormResponse; import org.signal.chat.backup.ListMediaRequest; @@ -64,6 +66,16 @@ public class BackupsAnonymousGrpcService extends ReactorBackupsAnonymousGrpc.Bac .map(credentials -> GetCdnCredentialsResponse.newBuilder().putAllHeaders(credentials).build()); } + @Override + public Mono getSvrBCredentials(final GetSvrBCredentialsRequest request) { + return authenticateBackupUserMono(request.getSignedPresentation()) + .map(backupManager::generateSvrbAuth) + .map(credentials -> GetSvrBCredentialsResponse.newBuilder() + .setUsername(credentials.username()) + .setPassword(credentials.password()) + .build()); + } + @Override public Mono getBackupInfo(final GetBackupInfoRequest request) { return Mono.fromFuture(() -> diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceDefinitions.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceDefinitions.java index d0b6700dc..6576a8fe3 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceDefinitions.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceDefinitions.java @@ -47,16 +47,6 @@ enum ExternalServiceDefinitions { .withClock(clock) .build(); }), - SVRB(ExternalServiceType.EXTERNAL_SERVICE_TYPE_SVRB, (chatConfig, clock) -> { - final SecureValueRecoveryConfiguration cfg = chatConfig.getSvrbConfiguration(); - return ExternalServiceCredentialsGenerator - .builder(cfg.userAuthenticationTokenSharedSecret()) - .withUserDerivationKey(cfg.userIdTokenSharedSecret().value()) - .prependUsername(false) - .withDerivedUsernameTruncateLength(16) - .withClock(clock) - .build(); - }), STORAGE(ExternalServiceType.EXTERNAL_SERVICE_TYPE_STORAGE, (chatConfig, clock) -> { final PaymentsServiceConfiguration cfg = chatConfig.getPaymentsServiceConfiguration(); return ExternalServiceCredentialsGenerator diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryClient.java index c33cefed3..c3727a7e1 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryClient.java @@ -30,9 +30,10 @@ import org.whispersystems.textsecuregcm.http.FaultTolerantHttpClient; import org.whispersystems.textsecuregcm.util.HttpUtils; /** - * A client for sending requests to Signal's secure value recovery v2 service on behalf of authenticated users. + * A client for sending requests to Signal's secure value recovery service on behalf of authenticated users. */ public class SecureValueRecoveryClient { + private static final Logger logger = LoggerFactory.getLogger(SecureValueRecoveryClient.class); private final ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator; @@ -67,8 +68,12 @@ public class SecureValueRecoveryClient { } public CompletableFuture removeData(final UUID accountUuid) { + return removeData(accountUuid.toString()); + } - final ExternalServiceCredentials credentials = secureValueRecoveryCredentialsGenerator.generateForUuid(accountUuid); + public CompletableFuture removeData(final String userIdentifier) { + + final ExternalServiceCredentials credentials = secureValueRecoveryCredentialsGenerator.generateFor(userIdentifier); final HttpRequest request = HttpRequest.newBuilder() .uri(deleteUri) @@ -83,11 +88,11 @@ public class SecureValueRecoveryClient { final List allowedErrors = allowedDeletionErrorStatusCodes.get(); if (allowedErrors.contains(response.statusCode())) { - logger.warn("Ignoring failure to delete svr entry for account {} with status {}", - accountUuid, response.statusCode()); + logger.warn("Ignoring failure to delete svr entry for identifier {} with status {}", + userIdentifier, response.statusCode()); return null; } - logger.warn("Failed to delete svr entry for account {} with status {}", accountUuid, response.statusCode()); + logger.warn("Failed to delete svr entry for identifier {} with status {}", userIdentifier, response.statusCode()); throw new SecureValueRecoveryException("Failed to delete backup", String.valueOf(response.statusCode())); }); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java index 4cdedcd3b..ad7381d61 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java @@ -128,7 +128,6 @@ public class AccountsManager extends RedisPubSubAdapter implemen private final ProfilesManager profilesManager; private final SecureStorageClient secureStorageClient; private final SecureValueRecoveryClient secureValueRecovery2Client; - private final SecureValueRecoveryClient secureValueRecoveryBClient; private final DisconnectionRequestManager disconnectionRequestManager; private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager; private final ClientPublicKeysManager clientPublicKeysManager; @@ -219,7 +218,6 @@ public class AccountsManager extends RedisPubSubAdapter implemen final ProfilesManager profilesManager, final SecureStorageClient secureStorageClient, final SecureValueRecoveryClient secureValueRecovery2Client, - final SecureValueRecoveryClient secureValueRecoveryBClient, final DisconnectionRequestManager disconnectionRequestManager, final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager, final ClientPublicKeysManager clientPublicKeysManager, @@ -238,7 +236,6 @@ public class AccountsManager extends RedisPubSubAdapter implemen this.profilesManager = profilesManager; this.secureStorageClient = secureStorageClient; this.secureValueRecovery2Client = secureValueRecovery2Client; - this.secureValueRecoveryBClient = secureValueRecoveryBClient; this.disconnectionRequestManager = disconnectionRequestManager; this.registrationRecoveryPasswordsManager = requireNonNull(registrationRecoveryPasswordsManager); this.clientPublicKeysManager = clientPublicKeysManager; @@ -1232,7 +1229,6 @@ public class AccountsManager extends RedisPubSubAdapter implemen return CompletableFuture.allOf( secureStorageClient.deleteStoredData(account.getUuid()), secureValueRecovery2Client.removeData(account.getUuid()), - secureValueRecoveryBClient.removeData(account.getUuid()), keysManager.deleteSingleUsePreKeys(account.getUuid()), keysManager.deleteSingleUsePreKeys(account.getPhoneNumberIdentifier()), messagesManager.clear(account.getUuid()), diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/CommandDependencies.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/CommandDependencies.java index a021eee62..1a1abb3a2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/CommandDependencies.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/CommandDependencies.java @@ -30,11 +30,11 @@ import org.whispersystems.textsecuregcm.backup.BackupManager; import org.whispersystems.textsecuregcm.backup.BackupsDb; import org.whispersystems.textsecuregcm.backup.Cdn3BackupCredentialGenerator; import org.whispersystems.textsecuregcm.backup.Cdn3RemoteStorageManager; +import org.whispersystems.textsecuregcm.backup.SecureValueRecoveryBCredentialsGeneratorFactory; import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.controllers.SecureStorageController; import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller; import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager; -import org.whispersystems.textsecuregcm.controllers.SecureValueRecoveryBController; import org.whispersystems.textsecuregcm.experiment.PushNotificationExperimentSamples; import org.whispersystems.textsecuregcm.grpc.net.GrpcClientConnectionManager; import org.whispersystems.textsecuregcm.limits.RateLimiters; @@ -174,8 +174,8 @@ record CommandDependencies( configuration.getSecureStorageServiceConfiguration()); ExternalServiceCredentialsGenerator secureValueRecovery2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator( configuration.getSvr2Configuration()); - ExternalServiceCredentialsGenerator secureValueRecoveryBCredentialsGenerator = SecureValueRecoveryBController.credentialsGenerator( - configuration.getSvrbConfiguration()); + ExternalServiceCredentialsGenerator secureValueRecoveryBCredentialsGenerator = + SecureValueRecoveryBCredentialsGeneratorFactory.svrbCredentialsGenerator(configuration.getSvrbConfiguration()); final ExecutorService awsSdkMetricsExecutor = environment.lifecycle() .virtualExecutorService(MetricRegistry.name(WhisperServerService.class, "awsSdkMetrics-%d")); @@ -276,7 +276,7 @@ record CommandDependencies( new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords); AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster, pubsubClient, accountLockManager, keys, messagesManager, profilesManager, - secureStorageClient, secureValueRecovery2Client, secureValueRecoveryBClient, disconnectionRequestManager, + secureStorageClient, secureValueRecovery2Client, disconnectionRequestManager, registrationRecoveryPasswordsManager, clientPublicKeysManager, accountLockExecutor, messagePollExecutor, clock, configuration.getLinkDeviceSecretConfiguration().secret().value(), dynamicConfigurationManager); RateLimiters rateLimiters = RateLimiters.create(dynamicConfigurationManager, rateLimitersCluster); @@ -299,6 +299,8 @@ record CommandDependencies( remoteStorageHttpExecutor, remoteStorageRetryExecutor, configuration.getCdn3StorageManagerConfiguration()), + secureValueRecoveryBCredentialsGenerator, + secureValueRecoveryBClient, clock); final IssuedReceiptsManager issuedReceiptsManager = new IssuedReceiptsManager( diff --git a/service/src/main/proto/org/signal/chat/backups.proto b/service/src/main/proto/org/signal/chat/backups.proto index b7507dfc0..9320bf305 100644 --- a/service/src/main/proto/org/signal/chat/backups.proto +++ b/service/src/main/proto/org/signal/chat/backups.proto @@ -188,6 +188,11 @@ service BackupsAnonymous { */ rpc GetCdnCredentials(GetCdnCredentialsRequest) returns (GetCdnCredentialsResponse) {} + /** + * Retrieve credentials used to interact with the SecureValueRecoveryB service + */ + rpc GetSvrBCredentials(GetSvrBCredentialsRequest) returns (GetSvrBCredentialsResponse) {} + /** * Retrieve information about the currently stored backup */ @@ -291,6 +296,21 @@ message GetCdnCredentialsResponse { map headers = 1; } +message GetSvrBCredentialsRequest { + SignedPresentation signed_presentation = 1; +} +message GetSvrBCredentialsResponse { + /** + * A username that can be presented to authenticate with SVRB + */ + string username = 1; + + /** + * A password that can be presented to authenticate with SVRB + */ + string password = 2; +} + message GetBackupInfoRequest { SignedPresentation signed_presentation = 1; } diff --git a/service/src/main/proto/org/signal/chat/credentials.proto b/service/src/main/proto/org/signal/chat/credentials.proto index 0a8cbbce1..d35086026 100644 --- a/service/src/main/proto/org/signal/chat/credentials.proto +++ b/service/src/main/proto/org/signal/chat/credentials.proto @@ -53,7 +53,6 @@ enum ExternalServiceType { EXTERNAL_SERVICE_TYPE_PAYMENTS = 2; EXTERNAL_SERVICE_TYPE_STORAGE = 3; EXTERNAL_SERVICE_TYPE_SVR = 4; - EXTERNAL_SERVICE_TYPE_SVRB = 5; } message GetExternalServiceCredentialsRequest { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java index 18e0cb65b..a613fec6c 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java @@ -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(), "")); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java index 4eeb3ae04..241345d51 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java @@ -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 = diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryBControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryBControllerTest.java deleted file mode 100644 index 3a5e03272..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryBControllerTest.java +++ /dev/null @@ -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); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryClientTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryClientTest.java index 01a803892..75b6096ba 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryClientTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecoveryClientTest.java @@ -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)) diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountCreationDeletionIntegrationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountCreationDeletionIntegrationTest.java index 9048a7212..e9b452cd9 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountCreationDeletionIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountCreationDeletionIntegrationTest.java @@ -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, diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java index 0531d28a7..ab667e7e7 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerChangeNumberIntegrationTest.java @@ -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, diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java index 16d8494cd..f020702c8 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerConcurrentModificationIntegrationTest.java @@ -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), 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 6c6764be1..ec528bdce 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerDeviceTransferIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerDeviceTransferIntegrationTest.java @@ -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), diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java index 9a49318e7..f7c29ba62 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java @@ -134,7 +134,6 @@ class AccountsManagerTest { private RedisAdvancedClusterAsyncCommands 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, diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java index a872f21f9..c408462b6 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerUsernameIntegrationTest.java @@ -162,7 +162,6 @@ class AccountsManagerUsernameIntegrationTest { profileManager, mock(SecureStorageClient.class), mock(SecureValueRecoveryClient.class), - mock(SecureValueRecoveryClient.class), disconnectionRequestManager, mock(RegistrationRecoveryPasswordsManager.class), mock(ClientPublicKeysManager.class), diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AddRemoveDeviceIntegrationTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AddRemoveDeviceIntegrationTest.java index be38e071a..4b403ab67 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AddRemoveDeviceIntegrationTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AddRemoveDeviceIntegrationTest.java @@ -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,