Skip to content

Commit e8a1854

Browse files
authored
Move /v1/svrb/auth to /v1/archives/auth/svrb
1 parent f8d27d8 commit e8a1854

23 files changed

+243
-189
lines changed

service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
import org.whispersystems.textsecuregcm.backup.BackupsDb;
9494
import org.whispersystems.textsecuregcm.backup.Cdn3BackupCredentialGenerator;
9595
import org.whispersystems.textsecuregcm.backup.Cdn3RemoteStorageManager;
96+
import org.whispersystems.textsecuregcm.backup.SecureValueRecoveryBCredentialsGeneratorFactory;
9697
import org.whispersystems.textsecuregcm.badges.ConfiguredProfileBadgeConverter;
9798
import org.whispersystems.textsecuregcm.captcha.CaptchaChecker;
9899
import org.whispersystems.textsecuregcm.captcha.CaptchaClient;
@@ -126,7 +127,6 @@
126127
import org.whispersystems.textsecuregcm.controllers.RemoteConfigControllerV1;
127128
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
128129
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
129-
import org.whispersystems.textsecuregcm.controllers.SecureValueRecoveryBController;
130130
import org.whispersystems.textsecuregcm.controllers.StickerController;
131131
import org.whispersystems.textsecuregcm.controllers.SubscriptionController;
132132
import org.whispersystems.textsecuregcm.controllers.VerificationController;
@@ -595,9 +595,9 @@ public void run(WhisperServerConfiguration config, Environment environment) thro
595595
ExternalServiceCredentialsGenerator paymentsCredentialsGenerator = PaymentsController.credentialsGenerator(
596596
config.getPaymentsServiceConfiguration());
597597
ExternalServiceCredentialsGenerator svr2CredentialsGenerator = SecureValueRecovery2Controller.credentialsGenerator(
598-
config.getSvr2Configuration());
599-
ExternalServiceCredentialsGenerator svrbCredentialsGenerator = SecureValueRecoveryBController.credentialsGenerator(
600-
config.getSvrbConfiguration());
598+
config.getSvr2Configuration());
599+
ExternalServiceCredentialsGenerator svrbCredentialsGenerator =
600+
SecureValueRecoveryBCredentialsGeneratorFactory.svrbCredentialsGenerator(config.getSvrbConfiguration());
601601

602602
RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
603603
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
@@ -644,7 +644,7 @@ public void run(WhisperServerConfiguration config, Environment environment) thro
644644
new ClientPublicKeysManager(clientPublicKeys, accountLockManager, accountLockExecutor);
645645
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
646646
pubsubClient, accountLockManager, keysManager, messagesManager, profilesManager,
647-
secureStorageClient, secureValueRecovery2Client, secureValueRecoveryBClient, disconnectionRequestManager,
647+
secureStorageClient, secureValueRecovery2Client, disconnectionRequestManager,
648648
registrationRecoveryPasswordsManager, clientPublicKeysManager, accountLockExecutor, messagePollExecutor,
649649
clock, config.getLinkDeviceSecretConfiguration().secret().value(), dynamicConfigurationManager);
650650
RemoteConfigsManager remoteConfigsManager = new RemoteConfigsManager(remoteConfigs);
@@ -798,6 +798,8 @@ public void run(WhisperServerConfiguration config, Environment environment) thro
798798
tusAttachmentGenerator,
799799
cdn3BackupCredentialGenerator,
800800
cdn3RemoteStorageManager,
801+
svrbCredentialsGenerator,
802+
secureValueRecoveryBClient,
801803
clock);
802804

803805
final AppleDeviceChecks appleDeviceChecks = new AppleDeviceChecks(
@@ -1115,7 +1117,6 @@ protected void configureServer(final ServerBuilder<?> serverBuilder) {
11151117
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig(), clock),
11161118
new SecureStorageController(storageCredentialsGenerator),
11171119
new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager),
1118-
new SecureValueRecoveryBController(svrbCredentialsGenerator),
11191120
new StickerController(rateLimiters, config.getCdnConfiguration().credentials().accessKeyId().value(),
11201121
config.getCdnConfiguration().credentials().secretAccessKey().value(), config.getCdnConfiguration().region(),
11211122
config.getCdnConfiguration().bucket()),

service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import com.google.common.annotations.VisibleForTesting;
99
import io.dropwizard.util.DataSize;
1010
import io.grpc.Status;
11-
import io.grpc.StatusRuntimeException;
1211
import io.micrometer.core.instrument.DistributionSummary;
1312
import io.micrometer.core.instrument.Metrics;
1413
import io.micrometer.core.instrument.Tag;
@@ -19,6 +18,7 @@
1918
import java.time.Duration;
2019
import java.time.Instant;
2120
import java.util.Base64;
21+
import java.util.HexFormat;
2222
import java.util.List;
2323
import java.util.Map;
2424
import java.util.Optional;
@@ -38,9 +38,12 @@
3838
import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator;
3939
import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
4040
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
41+
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
42+
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
4143
import org.whispersystems.textsecuregcm.limits.RateLimiters;
4244
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
4345
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
46+
import org.whispersystems.textsecuregcm.securevaluerecovery.SecureValueRecoveryClient;
4447
import org.whispersystems.textsecuregcm.util.AsyncTimerUtil;
4548
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
4649
import org.whispersystems.textsecuregcm.util.Pair;
@@ -94,24 +97,29 @@ public class BackupManager {
9497
private final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator;
9598
private final RemoteStorageManager remoteStorageManager;
9699
private final SecureRandom secureRandom = new SecureRandom();
100+
private final ExternalServiceCredentialsGenerator secureValueRecoveryBCredentialsGenerator;
101+
private final SecureValueRecoveryClient secureValueRecoveryBClient;
97102
private final Clock clock;
98103

99-
100104
public BackupManager(
101105
final BackupsDb backupsDb,
102106
final GenericServerSecretParams serverSecretParams,
103107
final RateLimiters rateLimiters,
104108
final TusAttachmentGenerator tusAttachmentGenerator,
105109
final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator,
106110
final RemoteStorageManager remoteStorageManager,
111+
final ExternalServiceCredentialsGenerator secureValueRecoveryBCredentialsGenerator,
112+
final SecureValueRecoveryClient secureValueRecoveryBClient,
107113
final Clock clock) {
108114
this.backupsDb = backupsDb;
109115
this.serverSecretParams = serverSecretParams;
110116
this.rateLimiters = rateLimiters;
111117
this.tusAttachmentGenerator = tusAttachmentGenerator;
112118
this.cdn3BackupCredentialGenerator = cdn3BackupCredentialGenerator;
113119
this.remoteStorageManager = remoteStorageManager;
120+
this.secureValueRecoveryBClient = secureValueRecoveryBClient;
114121
this.clock = clock;
122+
this.secureValueRecoveryBCredentialsGenerator = secureValueRecoveryBCredentialsGenerator;
115123
}
116124

117125

@@ -387,6 +395,26 @@ public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backup
387395
return cdn3BackupCredentialGenerator.readHeaders(backupUser.backupDir());
388396
}
389397

398+
/**
399+
* Generate credentials that can be used with SVRB
400+
*
401+
* @param backupUser an already ZK authenticated backup user
402+
* @return the credential that may be used with SVRB
403+
*/
404+
public ExternalServiceCredentials generateSvrbAuth(final AuthenticatedBackupUser backupUser) {
405+
checkBackupLevel(backupUser, BackupLevel.FREE);
406+
// Clients may only use SVRB with their messages backup-id
407+
checkBackupCredentialType(backupUser, BackupCredentialType.MESSAGES);
408+
return secureValueRecoveryBCredentialsGenerator.generateFor(svrbIdentifier(backupUser));
409+
}
410+
411+
private static String svrbIdentifier(final AuthenticatedBackupUser backupUser) {
412+
return svrbIdentifier(BackupsDb.hashedBackupId(backupUser.backupId()));
413+
}
414+
415+
private static String svrbIdentifier(final byte[] hashedBackupId) {
416+
return HexFormat.of().formatHex(hashedBackupId);
417+
}
390418

391419
/**
392420
* List of media stored for a particular backup id
@@ -427,13 +455,19 @@ public CompletionStage<ListMediaResult> list(
427455

428456
public CompletableFuture<Void> deleteEntireBackup(final AuthenticatedBackupUser backupUser) {
429457
checkBackupLevel(backupUser, BackupLevel.FREE);
430-
return backupsDb
458+
459+
// Clients only include SVRB data with their messages backup-id
460+
final CompletableFuture<Void> svrbRemoval = switch(backupUser.credentialType()) {
461+
case BackupCredentialType.MESSAGES -> secureValueRecoveryBClient.removeData(svrbIdentifier(backupUser));
462+
case BackupCredentialType.MEDIA -> CompletableFuture.completedFuture(null);
463+
};
464+
return svrbRemoval.thenCompose(_ -> backupsDb
431465
// Try to swap out the backupDir for the user
432466
.scheduleBackupDeletion(backupUser)
433467
// If there was already a pending swap, try to delete the cdn objects directly
434468
.exceptionallyCompose(ExceptionUtils.exceptionallyHandler(BackupsDb.PendingDeletionException.class, e ->
435469
AsyncTimerUtil.record(SYNCHRONOUS_DELETE_TIMER, () ->
436-
deletePrefix(backupUser.backupDir(), DELETION_CONCURRENCY))));
470+
deletePrefix(backupUser.backupDir(), DELETION_CONCURRENCY)))));
437471
}
438472

439473

@@ -617,12 +651,17 @@ public Flux<ExpiredBackup> getExpiredBackups(final int segments, final Scheduler
617651
* @return A stage that completes when the deletion operation is finished
618652
*/
619653
public CompletableFuture<Void> expireBackup(final ExpiredBackup expiredBackup) {
620-
return backupsDb.startExpiration(expiredBackup)
654+
// Clients only include SVRB data with their messages backup-id
655+
final CompletableFuture<Void> svrbRemoval = switch(expiredBackup.expirationType()) {
656+
case ALL -> secureValueRecoveryBClient.removeData(svrbIdentifier(expiredBackup.hashedBackupId()));
657+
case MEDIA, GARBAGE_COLLECTION -> CompletableFuture.completedFuture(null);
658+
};
659+
return svrbRemoval.thenCompose(_ -> backupsDb.startExpiration(expiredBackup)
621660
// the deletion operation is effectively single threaded -- it's expected that the caller can increase
622661
// concurrency by deleting more backups at once, rather than increasing concurrency deleting an individual
623662
// backup
624663
.thenCompose(ignored -> deletePrefix(expiredBackup.prefixToDelete(), 1))
625-
.thenCompose(ignored -> backupsDb.finishExpiration(expiredBackup));
664+
.thenCompose(ignored -> backupsDb.finishExpiration(expiredBackup)));
626665
}
627666

628667
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2025 Signal Messenger, LLC
3+
* SPDX-License-Identifier: AGPL-3.0-only
4+
*/
5+
6+
package org.whispersystems.textsecuregcm.backup;
7+
8+
import com.google.common.annotations.VisibleForTesting;
9+
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
10+
import org.whispersystems.textsecuregcm.configuration.SecureValueRecoveryConfiguration;
11+
import java.time.Clock;
12+
13+
public class SecureValueRecoveryBCredentialsGeneratorFactory {
14+
private SecureValueRecoveryBCredentialsGeneratorFactory() {}
15+
16+
17+
@VisibleForTesting
18+
static ExternalServiceCredentialsGenerator svrbCredentialsGenerator(
19+
final SecureValueRecoveryConfiguration cfg,
20+
final Clock clock) {
21+
return ExternalServiceCredentialsGenerator
22+
.builder(cfg.userAuthenticationTokenSharedSecret())
23+
.withUserDerivationKey(cfg.userIdTokenSharedSecret().value())
24+
.prependUsername(false)
25+
.withDerivedUsernameTruncateLength(16)
26+
.withClock(clock)
27+
.build();
28+
}
29+
30+
public static ExternalServiceCredentialsGenerator svrbCredentialsGenerator(final SecureValueRecoveryConfiguration cfg) {
31+
return svrbCredentialsGenerator(cfg, Clock.systemUTC());
32+
}
33+
}

service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
6565
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
6666
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
67+
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
6768
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
6869
import org.whispersystems.textsecuregcm.backup.BackupManager;
6970
import org.whispersystems.textsecuregcm.backup.CopyParameters;
@@ -389,6 +390,35 @@ public CompletionStage<ReadAuthResponse> readAuth(
389390
.thenApply(ReadAuthResponse::new);
390391
}
391392

393+
@GET
394+
@Path("/auth/svrb")
395+
@Produces(MediaType.APPLICATION_JSON)
396+
@Operation(
397+
summary = "Generate credentials for SVRB",
398+
description = """
399+
Generate SVRB service credentials. Generated credentials have an expiration time of 1 day (subject to change)
400+
""")
401+
@ApiResponse(responseCode = "200", description = "`JSON` with generated credentials.", useReturnTypeSchema = true)
402+
@ApiResponseZkAuth
403+
public CompletionStage<ExternalServiceCredentials> svrbAuth(
404+
@Auth final Optional<AuthenticatedDevice> account,
405+
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
406+
407+
@Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
408+
@NotNull
409+
@HeaderParam(X_SIGNAL_ZK_AUTH) final ArchiveController.BackupAuthCredentialPresentationHeader presentation,
410+
411+
@Parameter(description = BackupAuthCredentialPresentationSignature.DESCRIPTION, schema = @Schema(implementation = String.class))
412+
@NotNull
413+
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature) {
414+
if (account.isPresent()) {
415+
throw new BadRequestException("must not use authenticated connection for anonymous operations");
416+
}
417+
return backupManager
418+
.authenticateBackupUser(presentation.presentation, signature.signature, userAgent)
419+
.thenApply(backupManager::generateSvrbAuth);
420+
}
421+
392422
public record BackupInfoResponse(
393423
@Schema(description = "The CDN type where the message backup is stored. Media may be stored elsewhere.")
394424
int cdn,

service/src/main/java/org/whispersystems/textsecuregcm/controllers/SecureValueRecoveryBController.java

Lines changed: 0 additions & 63 deletions
This file was deleted.

service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import org.signal.chat.backup.GetBackupInfoResponse;
2222
import org.signal.chat.backup.GetCdnCredentialsRequest;
2323
import org.signal.chat.backup.GetCdnCredentialsResponse;
24+
import org.signal.chat.backup.GetSvrBCredentialsRequest;
25+
import org.signal.chat.backup.GetSvrBCredentialsResponse;
2426
import org.signal.chat.backup.GetUploadFormRequest;
2527
import org.signal.chat.backup.GetUploadFormResponse;
2628
import org.signal.chat.backup.ListMediaRequest;
@@ -64,6 +66,16 @@ public Mono<GetCdnCredentialsResponse> getCdnCredentials(final GetCdnCredentials
6466
.map(credentials -> GetCdnCredentialsResponse.newBuilder().putAllHeaders(credentials).build());
6567
}
6668

69+
@Override
70+
public Mono<GetSvrBCredentialsResponse> getSvrBCredentials(final GetSvrBCredentialsRequest request) {
71+
return authenticateBackupUserMono(request.getSignedPresentation())
72+
.map(backupManager::generateSvrbAuth)
73+
.map(credentials -> GetSvrBCredentialsResponse.newBuilder()
74+
.setUsername(credentials.username())
75+
.setPassword(credentials.password())
76+
.build());
77+
}
78+
6779
@Override
6880
public Mono<GetBackupInfoResponse> getBackupInfo(final GetBackupInfoRequest request) {
6981
return Mono.fromFuture(() ->

service/src/main/java/org/whispersystems/textsecuregcm/grpc/ExternalServiceDefinitions.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,6 @@ enum ExternalServiceDefinitions {
4747
.withClock(clock)
4848
.build();
4949
}),
50-
SVRB(ExternalServiceType.EXTERNAL_SERVICE_TYPE_SVRB, (chatConfig, clock) -> {
51-
final SecureValueRecoveryConfiguration cfg = chatConfig.getSvrbConfiguration();
52-
return ExternalServiceCredentialsGenerator
53-
.builder(cfg.userAuthenticationTokenSharedSecret())
54-
.withUserDerivationKey(cfg.userIdTokenSharedSecret().value())
55-
.prependUsername(false)
56-
.withDerivedUsernameTruncateLength(16)
57-
.withClock(clock)
58-
.build();
59-
}),
6050
STORAGE(ExternalServiceType.EXTERNAL_SERVICE_TYPE_STORAGE, (chatConfig, clock) -> {
6151
final PaymentsServiceConfiguration cfg = chatConfig.getPaymentsServiceConfiguration();
6252
return ExternalServiceCredentialsGenerator

0 commit comments

Comments
 (0)