From d6a0129c5adcea11a2e1fe646a8faf7324e95146 Mon Sep 17 00:00:00 2001 From: Ravi Khadiwala Date: Fri, 13 Feb 2026 13:37:03 -0600 Subject: [PATCH] Treat missing backup after authentication as an authentication failure --- .../whispersystems/textsecuregcm/backup/BackupManager.java | 7 +++---- .../org/whispersystems/textsecuregcm/backup/BackupsDb.java | 4 +++- .../textsecuregcm/controllers/ArchiveController.java | 3 +-- .../textsecuregcm/grpc/BackupsAnonymousGrpcService.java | 3 +-- .../textsecuregcm/backup/BackupManagerTest.java | 2 +- .../whispersystems/textsecuregcm/backup/BackupsDbTest.java | 2 +- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java index 65fa5d09b..b42f288d4 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java @@ -230,13 +230,12 @@ public class BackupManager { * @param backupUser an already ZK authenticated backup user * @return Information about the existing backup * @throws BackupPermissionException if the credential does not have the correct level - * @throws BackupNotFoundException if the provided backupuser does not exist + * @throws BackupFailedZkAuthenticationException if the provided backupuser does not exist */ - public BackupInfo backupInfo(final AuthenticatedBackupUser backupUser) - throws BackupNotFoundException, BackupPermissionException { + public BackupInfo backupInfo(final AuthenticatedBackupUser backupUser) throws BackupPermissionException, BackupFailedZkAuthenticationException { checkBackupLevel(backupUser, BackupLevel.FREE); final BackupsDb.BackupDescription backupDescription = ExceptionUtils.unwrapSupply( - BackupNotFoundException.class, + BackupFailedZkAuthenticationException.class, () -> backupsDb.describeBackup(backupUser).join()); return new BackupInfo( backupDescription.cdn(), diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java index 86be0eb8e..25a599087 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java @@ -331,7 +331,9 @@ public class BackupsDb { .build()) .thenApply(response -> { if (!response.hasItem()) { - throw ExceptionUtils.wrap(new BackupNotFoundException("Backup ID not found")); + // At this point, the user has already authenticated against this backup record, so we must have raced + // with a deletion. Just throw the same error we would have thrown if authentication had failed + throw ExceptionUtils.wrap(new BackupFailedZkAuthenticationException("Backup ID not found")); } // If the client hasn't already uploaded a backup, return the cdn we would return if they did create one final int cdn = AttributeValues.getInt(response.item(), ATTR_CDN, BACKUP_CDN); 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 afa48ffff..500fcfc5d 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java @@ -486,7 +486,6 @@ public class ArchiveController { summary = "Fetch backup info", description = "Retrieve information about the currently stored backup") @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = BackupInfoResponse.class))) - @ApiResponse(responseCode = "404", description = "No existing backups found") @ApiResponse(responseCode = "429", description = "Rate limited.") @ApiResponseZkAuth @ManagedAsync @@ -501,7 +500,7 @@ public class ArchiveController { @Parameter(description = BackupAuthCredentialPresentationSignature.DESCRIPTION, schema = @Schema(implementation = String.class)) @NotNull @HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature) - throws BackupFailedZkAuthenticationException, BackupNotFoundException, BackupPermissionException { + throws BackupFailedZkAuthenticationException, BackupPermissionException { if (account.isPresent()) { throw new BadRequestException("must not use authenticated connection for anonymous operations"); } 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 0ed629d94..4ddad2e38 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/BackupsAnonymousGrpcService.java @@ -99,8 +99,7 @@ public class BackupsAnonymousGrpcService extends SimpleBackupsAnonymousGrpc.Back } @Override - public GetBackupInfoResponse getBackupInfo(final GetBackupInfoRequest request) - throws BackupNotFoundException, BackupPermissionException { + public GetBackupInfoResponse getBackupInfo(final GetBackupInfoRequest request) throws BackupPermissionException { try { final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation()); final BackupManager.BackupInfo info = backupManager.backupInfo(backupUser); 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 511f3c467..1d8304e88 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java @@ -961,7 +961,7 @@ public class BackupManagerTest { if (expirationType == ExpiredBackup.ExpirationType.ALL) { // should have deleted the db row for the backup CompletableFutureTestUtil.assertFailsWithCause( - BackupNotFoundException.class, + BackupFailedZkAuthenticationException.class, backupsDb.describeBackup(backupUser)); } else { // should have deleted all the media, but left the backup descriptor in place diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java index 2c0175a92..b0e9d2fb4 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java @@ -211,7 +211,7 @@ public class BackupsDbTest { // The backup entry should be gone CompletableFutureTestUtil.assertFailsWithCause( - BackupNotFoundException.class, + BackupFailedZkAuthenticationException.class, backupsDb.describeBackup(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID))); assertThat(expiredBackups.apply(Instant.ofEpochSecond(10))).isEmpty(); }