Only accept backup receipt redemption when account has a backup credential request

This commit is contained in:
Ravi Khadiwala 2025-02-19 16:37:36 -06:00 committed by ravi-signal
parent 093ac6fb16
commit ec79386306
4 changed files with 34 additions and 0 deletions

View File

@ -236,6 +236,12 @@ public class BackupAuthManager {
.asRuntimeException(); .asRuntimeException();
} }
if (account.getBackupCredentialRequest(BackupCredentialType.MEDIA).isEmpty()) {
throw Status.ABORTED
.withDescription("account must have a backup-id commitment")
.asRuntimeException();
}
return redeemedReceiptsManager return redeemedReceiptsManager
.put(receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, account.getUuid()) .put(receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, account.getUuid())
.thenCompose(receiptAllowed -> { .thenCompose(receiptAllowed -> {

View File

@ -173,9 +173,13 @@ public class ArchiveController {
After successful redemption, subsequent requests to /v1/archive/auth will return credentials with the level on After successful redemption, subsequent requests to /v1/archive/auth will return credentials with the level on
the provided receipt until the expiration time on the receipt. the provided receipt until the expiration time on the receipt.
Accounts must have an existing backup credential request in order to redeem a receipt. This request will fail
if the account has not already set a backup credential request via PUT `/v1/archives/backupid`.
""") """)
@ApiResponse(responseCode = "204", description = "The receipt was redeemed") @ApiResponse(responseCode = "204", description = "The receipt was redeemed")
@ApiResponse(responseCode = "400", description = "The provided presentation or receipt was invalid") @ApiResponse(responseCode = "400", description = "The provided presentation or receipt was invalid")
@ApiResponse(responseCode = "409", description = "The target account does not have a backup-id commitment")
@ApiResponse(responseCode = "429", description = "Rate limited.") @ApiResponse(responseCode = "429", description = "Rate limited.")
public CompletionStage<Response> redeemReceipt( public CompletionStage<Response> redeemReceipt(
@Mutable @Auth final AuthenticatedDevice account, @Mutable @Auth final AuthenticatedDevice account,

View File

@ -55,6 +55,9 @@ service Backups {
* After successful redemption, subsequent requests to * After successful redemption, subsequent requests to
* GetBackupAuthCredentials will return credentials with the level on the * GetBackupAuthCredentials will return credentials with the level on the
* provided receipt until the expiration time on the receipt. * provided receipt until the expiration time on the receipt.
*
* errors:
* ABORTED: The target account does not have a backup-id commitment
*/ */
rpc RedeemReceipt(RedeemReceiptRequest) returns (RedeemReceiptResponse) {} rpc RedeemReceipt(RedeemReceiptRequest) returns (RedeemReceiptResponse) {}

View File

@ -348,6 +348,7 @@ public class BackupAuthManagerTest {
final BackupAuthManager authManager = create(BackupLevel.FREE); final BackupAuthManager authManager = create(BackupLevel.FREE);
final Account account = mock(Account.class); final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(aci); when(account.getUuid()).thenReturn(aci);
when(account.getBackupCredentialRequest(BackupCredentialType.MEDIA)).thenReturn(Optional.of(new byte[0]));
clock.pin(Instant.EPOCH.plus(Duration.ofDays(1))); clock.pin(Instant.EPOCH.plus(Duration.ofDays(1)));
when(accountsManager.updateAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(account)); when(accountsManager.updateAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(account));
@ -357,6 +358,24 @@ public class BackupAuthManagerTest {
verify(accountsManager, times(1)).updateAsync(any(), any()); verify(accountsManager, times(1)).updateAsync(any(), any());
} }
@Test
void redeemReceiptNoBackupRequest() {
final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1));
final BackupAuthManager authManager = create(BackupLevel.FREE);
final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(aci);
when(account.getBackupCredentialRequest(BackupCredentialType.MEDIA)).thenReturn(Optional.empty());
clock.pin(Instant.EPOCH.plus(Duration.ofDays(1)));
when(redeemedReceiptsManager.put(any(), eq(expirationTime.getEpochSecond()), eq(201L), eq(aci)))
.thenReturn(CompletableFuture.completedFuture(true));
assertThatExceptionOfType(StatusRuntimeException.class)
.isThrownBy(() ->
authManager.redeemReceipt(account, receiptPresentation(201, expirationTime)).join())
.extracting(ex -> ex.getStatus().getCode())
.isEqualTo(Status.Code.ABORTED);
}
@Test @Test
void mergeRedemptions() throws InvalidInputException, VerificationFailedException { void mergeRedemptions() throws InvalidInputException, VerificationFailedException {
final Instant newExpirationTime = Instant.EPOCH.plus(Duration.ofDays(1)); final Instant newExpirationTime = Instant.EPOCH.plus(Duration.ofDays(1));
@ -365,6 +384,7 @@ public class BackupAuthManagerTest {
final BackupAuthManager authManager = create(BackupLevel.FREE); final BackupAuthManager authManager = create(BackupLevel.FREE);
final Account account = mock(Account.class); final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(aci); when(account.getUuid()).thenReturn(aci);
when(account.getBackupCredentialRequest(BackupCredentialType.MEDIA)).thenReturn(Optional.of(new byte[0]));
// The account has an existing voucher with a later expiration date // The account has an existing voucher with a later expiration date
when(account.getBackupVoucher()).thenReturn(new Account.BackupVoucher(201, existingExpirationTime)); when(account.getBackupVoucher()).thenReturn(new Account.BackupVoucher(201, existingExpirationTime));
@ -429,6 +449,7 @@ public class BackupAuthManagerTest {
final BackupAuthManager authManager = create(BackupLevel.FREE); final BackupAuthManager authManager = create(BackupLevel.FREE);
final Account account = mock(Account.class); final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(aci); when(account.getUuid()).thenReturn(aci);
when(account.getBackupCredentialRequest(BackupCredentialType.MEDIA)).thenReturn(Optional.of(new byte[0]));
clock.pin(Instant.EPOCH.plus(Duration.ofDays(1))); clock.pin(Instant.EPOCH.plus(Duration.ofDays(1)));
when(accountsManager.updateAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(account)); when(accountsManager.updateAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(account));