From 19944bfdb2a48e3c854a08069472c747ded9116c Mon Sep 17 00:00:00 2001 From: Ravi Khadiwala Date: Tue, 23 Apr 2024 17:53:05 -0500 Subject: [PATCH] Update to libsignal 0.45 and use libsignal's BackupLevel --- pom.xml | 2 +- .../auth/AuthenticatedBackupUser.java | 4 +- .../backup/BackupAuthManager.java | 30 +++--- .../textsecuregcm/backup/BackupLevelUtil.java | 17 +++ .../textsecuregcm/backup/BackupManager.java | 53 ++++----- .../textsecuregcm/backup/BackupTier.java | 40 ------- .../textsecuregcm/backup/BackupsDb.java | 39 +++---- .../textsecuregcm/backup/ExpiredBackup.java | 1 - .../SubscriptionConfiguration.java | 16 ++- .../backup/BackupAuthManagerTest.java | 76 +++++++------ .../backup/BackupAuthTestUtil.java | 19 ++-- .../backup/BackupManagerTest.java | 101 +++++++++--------- .../textsecuregcm/backup/BackupsDbTest.java | 57 +++++----- .../backup/Cdn3RemoteStorageManagerTest.java | 18 ++-- .../controllers/ArchiveControllerTest.java | 62 +++++------ 15 files changed, 257 insertions(+), 278 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupLevelUtil.java delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupTier.java diff --git a/pom.xml b/pom.xml index 6b11fed7a..81817d6ea 100644 --- a/pom.xml +++ b/pom.xml @@ -278,7 +278,7 @@ org.signal libsignal-server - 0.44.0 + 0.45.0 org.signal.forks diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedBackupUser.java b/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedBackupUser.java index 0cb9bb7f8..8f20e2062 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedBackupUser.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/auth/AuthenticatedBackupUser.java @@ -5,6 +5,6 @@ package org.whispersystems.textsecuregcm.auth; -import org.whispersystems.textsecuregcm.backup.BackupTier; +import org.signal.libsignal.zkgroup.backups.BackupLevel; -public record AuthenticatedBackupUser(byte[] backupId, BackupTier backupTier, String backupDir, String mediaDir) {} +public record AuthenticatedBackupUser(byte[] backupId, BackupLevel backupLevel, String backupDir, String mediaDir) {} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java index 5c4a60d33..74e9a4eec 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupAuthManager.java @@ -11,7 +11,6 @@ import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -21,6 +20,7 @@ import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest; import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialResponse; +import org.signal.libsignal.zkgroup.backups.BackupLevel; import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; import org.signal.libsignal.zkgroup.receipts.ReceiptSerial; import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations; @@ -89,7 +89,7 @@ public BackupAuthManager( */ public CompletableFuture commitBackupId(final Account account, final BackupAuthCredentialRequest backupAuthCredentialRequest) throws RateLimitExceededException { - if (configuredReceiptLevel(account).isEmpty()) { + if (configuredBackupLevel(account).isEmpty()) { throw Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException(); } @@ -141,7 +141,7 @@ public CompletableFuture> getBackupAuthCredentials( } // If this account isn't allowed some level of backup access via configuration, don't continue - final long configuredReceiptLevel = configuredReceiptLevel(account).orElseThrow(() -> + final BackupLevel configuredBackupLevel = configuredBackupLevel(account).orElseThrow(() -> Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException()); final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS); @@ -169,9 +169,9 @@ public CompletableFuture> getBackupAuthCredentials( .map(redemptionTime -> { // Check if the account has a voucher that's good for a certain receiptLevel at redemption time, otherwise // use the default receipt level - final long receiptLevel = storedReceiptLevel(account, redemptionTime).orElse(configuredReceiptLevel); + final BackupLevel backupLevel = storedBackupLevel(account, redemptionTime).orElse(configuredBackupLevel); return new Credential( - credentialReq.issueCredential(redemptionTime, receiptLevel, serverSecretParams), + credentialReq.issueCredential(redemptionTime, backupLevel, serverSecretParams), redemptionTime); }) .toList()); @@ -208,10 +208,11 @@ public CompletableFuture redeemReceipt( final long receiptLevel = receiptCredentialPresentation.getReceiptLevel(); - BackupTier.fromReceiptLevel(receiptLevel).filter(BackupTier.MEDIA::equals) - .orElseThrow(() -> Status.INVALID_ARGUMENT - .withDescription("server does not recognize the requested receipt level") - .asRuntimeException()); + if (BackupLevelUtil.fromReceiptLevel(receiptLevel) != BackupLevel.MEDIA) { + throw Status.INVALID_ARGUMENT + .withDescription("server does not recognize the requested receipt level") + .asRuntimeException(); + } return redeemedReceiptsManager .put(receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, account.getUuid()) @@ -262,10 +263,11 @@ private boolean hasExpiredVoucher(final Account account) { * @param redemptionTime The time to check against the expiration time * @return The receipt level on the backup voucher, or empty if the account does not have one or it is expired */ - private Optional storedReceiptLevel(final Account account, final Instant redemptionTime) { + private Optional storedBackupLevel(final Account account, final Instant redemptionTime) { return Optional.ofNullable(account.getBackupVoucher()) .filter(backupVoucher -> !redemptionTime.isAfter(backupVoucher.expiration())) - .map(Account.BackupVoucher::receiptLevel); + .map(Account.BackupVoucher::receiptLevel) + .map(BackupLevelUtil::fromReceiptLevel); } /** @@ -275,12 +277,12 @@ private Optional storedReceiptLevel(final Account account, final Instant r * @return If present, the default receipt level that should be used for the account if the account does not have a * BackupVoucher. Empty if the account should never have backup access */ - private Optional configuredReceiptLevel(final Account account) { + private Optional configuredBackupLevel(final Account account) { if (inExperiment(BACKUP_MEDIA_EXPERIMENT_NAME, account)) { - return Optional.of(BackupTier.MEDIA.getReceiptLevel()); + return Optional.of(BackupLevel.MEDIA); } if (inExperiment(BACKUP_EXPERIMENT_NAME, account)) { - return Optional.of(BackupTier.MESSAGES.getReceiptLevel()); + return Optional.of(BackupLevel.MESSAGES); } return Optional.empty(); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupLevelUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupLevelUtil.java new file mode 100644 index 000000000..9c9c53ae4 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupLevelUtil.java @@ -0,0 +1,17 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ +package org.whispersystems.textsecuregcm.backup; + +import org.signal.libsignal.zkgroup.backups.BackupLevel; + +public class BackupLevelUtil { + public static BackupLevel fromReceiptLevel(long receiptLevel) { + try { + return BackupLevel.fromValue(Math.toIntExact(receiptLevel)); + } catch (ArithmeticException e) { + throw new IllegalArgumentException("Invalid receipt level: " + receiptLevel); + } + } +} 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 6dd785d6d..32d2d315a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupManager.java @@ -30,6 +30,7 @@ import org.signal.libsignal.zkgroup.GenericServerSecretParams; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation; +import org.signal.libsignal.zkgroup.backups.BackupLevel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator; @@ -126,14 +127,8 @@ public CompletableFuture setPublicKey( // Note: this is a special case where we can't validate the presentation signature against the stored public key // because we are currently setting it. We check against the provided public key, but we must also verify that // there isn't an existing, different stored public key for the backup-id (verified with a condition expression) - final BackupTier backupTier = verifyPresentation(presentation).verifySignature(signature, publicKey); - if (backupTier.compareTo(BackupTier.MESSAGES) < 0) { - Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment(); - throw Status.PERMISSION_DENIED - .withDescription("credential does not support setting public key") - .asRuntimeException(); - } - return backupsDb.setPublicKey(presentation.getBackupId(), backupTier, publicKey) + final BackupLevel backupLevel = verifyPresentation(presentation).verifySignature(signature, publicKey); + return backupsDb.setPublicKey(presentation.getBackupId(), backupLevel, publicKey) .exceptionally(ExceptionUtils.exceptionallyHandler(PublicKeyConflictException.class, ex -> { Metrics.counter(ZK_AUTHN_COUNTER_NAME, SUCCESS_TAG_NAME, String.valueOf(false), @@ -156,7 +151,7 @@ public CompletableFuture setPublicKey( */ public CompletableFuture createMessageBackupUploadDescriptor( final AuthenticatedBackupUser backupUser) { - checkBackupTier(backupUser, BackupTier.MESSAGES); + checkBackupLevel(backupUser, BackupLevel.MESSAGES); // this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp return backupsDb @@ -166,7 +161,7 @@ public CompletableFuture createMessageBackupUploadDescri public BackupUploadDescriptor createTemporaryAttachmentUploadDescriptor(final AuthenticatedBackupUser backupUser) throws RateLimitExceededException { - checkBackupTier(backupUser, BackupTier.MEDIA); + checkBackupLevel(backupUser, BackupLevel.MEDIA); RateLimiter.adaptLegacyException(() -> rateLimiters .forDescriptor(RateLimiters.For.BACKUP_ATTACHMENT) @@ -185,7 +180,7 @@ public BackupUploadDescriptor createTemporaryAttachmentUploadDescriptor(final Au * @param backupUser an already ZK authenticated backup user */ public CompletableFuture ttlRefresh(final AuthenticatedBackupUser backupUser) { - checkBackupTier(backupUser, BackupTier.MESSAGES); + checkBackupLevel(backupUser, BackupLevel.MESSAGES); // update message backup TTL return backupsDb.ttlRefresh(backupUser); } @@ -200,7 +195,7 @@ public record BackupInfo(int cdn, String backupSubdir, String mediaSubdir, Strin * @return Information about the existing backup */ public CompletableFuture backupInfo(final AuthenticatedBackupUser backupUser) { - checkBackupTier(backupUser, BackupTier.MESSAGES); + checkBackupLevel(backupUser, BackupLevel.MESSAGES); return backupsDb.describeBackup(backupUser) .thenApply(backupDescription -> new BackupInfo( backupDescription.cdn(), @@ -218,7 +213,7 @@ public CompletableFuture backupInfo(final AuthenticatedBackupUser ba * @return true if mediaLength bytes can be stored */ public CompletableFuture canStoreMedia(final AuthenticatedBackupUser backupUser, final long mediaLength) { - checkBackupTier(backupUser, BackupTier.MEDIA); + checkBackupLevel(backupUser, BackupLevel.MEDIA); return backupsDb.getMediaUsage(backupUser) .thenComposeAsync(info -> { final boolean canStore = MAX_TOTAL_BACKUP_MEDIA_BYTES - info.usageInfo().bytesUsed() >= mediaLength; @@ -269,7 +264,7 @@ public CompletableFuture copyToBackup( final int sourceLength, final MediaEncryptionParameters encryptionParameters, final byte[] destinationMediaId) { - checkBackupTier(backupUser, BackupTier.MEDIA); + checkBackupLevel(backupUser, BackupLevel.MEDIA); if (sourceLength > MAX_MEDIA_OBJECT_SIZE) { throw Status.INVALID_ARGUMENT .withDescription("Invalid sourceObject size") @@ -331,7 +326,7 @@ private URI attachmentReadUri(final int cdn, final String key) throws IOExceptio * @return A map of headers to include with CDN requests */ public Map generateReadAuth(final AuthenticatedBackupUser backupUser, final int cdnNumber) { - checkBackupTier(backupUser, BackupTier.MESSAGES); + checkBackupLevel(backupUser, BackupLevel.MESSAGES); if (cdnNumber != 3) { throw Status.INVALID_ARGUMENT.withDescription("unknown cdn").asRuntimeException(); } @@ -359,7 +354,7 @@ public CompletionStage list( final AuthenticatedBackupUser backupUser, final Optional cursor, final int limit) { - checkBackupTier(backupUser, BackupTier.MESSAGES); + checkBackupLevel(backupUser, BackupLevel.MESSAGES); return remoteStorageManager.list(cdnMediaDirectory(backupUser), cursor, limit) .thenApply(result -> new ListMediaResult( @@ -377,7 +372,7 @@ public CompletionStage list( } public CompletableFuture deleteEntireBackup(final AuthenticatedBackupUser backupUser) { - checkBackupTier(backupUser, BackupTier.MESSAGES); + checkBackupLevel(backupUser, BackupLevel.MESSAGES); return backupsDb // Try to swap out the backupDir for the user .scheduleBackupDeletion(backupUser) @@ -395,7 +390,7 @@ private record DeleteFailure(Throwable e) implements Either {} public CompletableFuture delete(final AuthenticatedBackupUser backupUser, final List storageDescriptors) { - checkBackupTier(backupUser, BackupTier.MESSAGES); + checkBackupLevel(backupUser, BackupLevel.MESSAGES); if (storageDescriptors.stream().anyMatch(sd -> sd.cdn() != remoteStorageManager.cdnNumber())) { throw Status.INVALID_ARGUMENT @@ -556,7 +551,7 @@ private CompletableFuture deletePrefix(final String prefixToDelete, int co interface PresentationSignatureVerifier { - BackupTier verifySignature(byte[] signature, ECPublicKey publicKey); + BackupLevel verifySignature(byte[] signature, ECPublicKey publicKey); } /** @@ -588,27 +583,19 @@ private PresentationSignatureVerifier verifyPresentation(final BackupAuthCredent .withDescription("backup auth credential presentation signature verification failed") .asRuntimeException(); } - return BackupTier - .fromReceiptLevel(presentation.getReceiptLevel()) - .orElseThrow(() -> { - Metrics.counter(ZK_AUTHN_COUNTER_NAME, - SUCCESS_TAG_NAME, String.valueOf(false), - FAILURE_REASON_TAG_NAME, "invalid_receipt_level") - .increment(); - return Status.PERMISSION_DENIED.withDescription("invalid receipt level").asRuntimeException(); - }); + return presentation.getBackupLevel(); }; } /** - * Check that the authenticated backup user is authorized to use the provided backupTier + * Check that the authenticated backup user is authorized to use the provided backupLevel * * @param backupUser The backup user to check - * @param backupTier The authorization level to verify the backupUser has access to - * @throws {@link Status#PERMISSION_DENIED} error if the backup user is not authorized to access {@code backupTier} + * @param backupLevel The authorization level to verify the backupUser has access to + * @throws {@link Status#PERMISSION_DENIED} error if the backup user is not authorized to access {@code backupLevel} */ - private static void checkBackupTier(final AuthenticatedBackupUser backupUser, final BackupTier backupTier) { - if (backupUser.backupTier().compareTo(backupTier) < 0) { + private static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel) { + if (backupUser.backupLevel().compareTo(backupLevel) < 0) { Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment(); throw Status.PERMISSION_DENIED .withDescription("credential does not support the requested operation") diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupTier.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupTier.java deleted file mode 100644 index 8d5061327..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupTier.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2023 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.backup; - -import java.util.Arrays; -import java.util.Map; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; - -/** - * Maps receipt levels to BackupTiers. Existing receipt levels should never be remapped to a different tier. - *

- * Today, receipt levels 1:1 correspond to tiers, but in the future multiple receipt levels may be accepted for access - * to a single tier. - */ -public enum BackupTier { - NONE(0), - MESSAGES(200), - MEDIA(201); - - private static Map LOOKUP = Arrays.stream(BackupTier.values()) - .collect(Collectors.toMap(BackupTier::getReceiptLevel, Function.identity())); - private long receiptLevel; - - BackupTier(long receiptLevel) { - this.receiptLevel = receiptLevel; - } - - long getReceiptLevel() { - return receiptLevel; - } - - public static Optional fromReceiptLevel(long receiptLevel) { - return Optional.ofNullable(LOOKUP.get(receiptLevel)); - } -} 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 e68e8df34..175c43adc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/BackupsDb.java @@ -23,6 +23,7 @@ import java.util.function.Predicate; import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.ecc.ECPublicKey; +import org.signal.libsignal.zkgroup.backups.BackupLevel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser; @@ -45,10 +46,10 @@ * Tracks backup metadata in a persistent store. *

* It's assumed that the caller has already validated that the backupUser being operated on has valid credentials and - * possesses the appropriate {@link BackupTier} to perform the current operation. + * possesses the appropriate {@link BackupLevel} to perform the current operation. *

* Backup records track two timestamps indicating the last time that a user interacted with their backup. One for the - * last refresh that contained a credential including media tier, and the other for any access. After a period of + * last refresh that contained a credential including media level, and the other for any access. After a period of * inactivity stale backups can be purged (either just the media, or the entire backup). Callers can discover what * backups are stale and whether only the media or the entire backup is stale via {@link #getExpiredBackups}. *

@@ -86,7 +87,7 @@ public class BackupsDb { // garbage collection of archive objects. public static final String ATTR_LAST_REFRESH = "R"; // N: Time in seconds since epoch of the last backup media refresh. This timestamp can only be updated if the client - // has BackupTier.MEDIA, and must be periodically updated to avoid garbage collection of media objects. + // has BackupLevel.MEDIA, and must be periodically updated to avoid garbage collection of media objects. public static final String ATTR_LAST_MEDIA_REFRESH = "MR"; // B: A 32 byte public key that should be used to sign the presentation used to authenticate requests against the // backup-id @@ -120,18 +121,18 @@ public BackupsDb( /** * Set the public key associated with a backupId. * - * @param authenticatedBackupId The backup-id bytes that should be associated with the provided public key - * @param authenticatedBackupTier The backup tier - * @param publicKey The public key to associate with the backup id + * @param authenticatedBackupId The backup-id bytes that should be associated with the provided public key + * @param authenticatedBackupLevel The backup level + * @param publicKey The public key to associate with the backup id * @return A stage that completes when the public key has been set. If the backup-id already has a set public key that * does not match, the stage will be completed exceptionally with a {@link PublicKeyConflictException} */ CompletableFuture setPublicKey( final byte[] authenticatedBackupId, - final BackupTier authenticatedBackupTier, + final BackupLevel authenticatedBackupLevel, final ECPublicKey publicKey) { final byte[] hashedBackupId = hashedBackupId(authenticatedBackupId); - return dynamoClient.updateItem(new UpdateBuilder(backupTableName, authenticatedBackupTier, hashedBackupId) + return dynamoClient.updateItem(new UpdateBuilder(backupTableName, authenticatedBackupLevel, hashedBackupId) .addSetExpression("#publicKey = :publicKey", Map.entry("#publicKey", ATTR_PUBLIC_KEY), Map.entry(":publicKey", AttributeValues.b(publicKey.serialize()))) @@ -284,7 +285,7 @@ CompletableFuture scheduleBackupDeletion(final AuthenticatedBackupUser bac final byte[] hashedBackupId = hashedBackupId(backupUser); // Clear usage metadata, swap names of things we intend to delete, and record our intent to delete in attr_expired_prefix - return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupTier.MEDIA, hashedBackupId) + return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, hashedBackupId) .clearMediaUsage(clock) .expireDirectoryNames(secureRandom, ExpiredBackup.ExpirationType.ALL) .setRefreshTimes(Instant.ofEpochSecond(0)) @@ -299,7 +300,7 @@ CompletableFuture scheduleBackupDeletion(final AuthenticatedBackupUser bac // is toggling backups on and off. In this case, it should be pretty cheap to directly delete the backup. // Instead of changing the backupDir, just make sure the row has expired/ timestamps and tell the caller we // couldn't schedule the deletion. - dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupTier.MEDIA, hashedBackupId) + dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, hashedBackupId) .setRefreshTimes(Instant.ofEpochSecond(0)) .updateItemBuilder() .build()) @@ -398,7 +399,7 @@ CompletableFuture startExpiration(final ExpiredBackup expiredBackup) { } // Clear usage metadata, swap names of things we intend to delete, and record our intent to delete in attr_expired_prefix - return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupTier.MEDIA, expiredBackup.hashedBackupId()) + return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, expiredBackup.hashedBackupId()) .clearMediaUsage(clock) .expireDirectoryNames(secureRandom, expiredBackup.expirationType()) .addRemoveExpression(Map.entry("#mediaRefresh", ATTR_LAST_MEDIA_REFRESH)) @@ -432,7 +433,7 @@ CompletableFuture finishExpiration(final ExpiredBackup expiredBackup) { .build()) .thenRun(Util.NOOP); } else { - return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupTier.MEDIA, hashedBackupId) + return dynamoClient.updateItem(new UpdateBuilder(backupTableName, BackupLevel.MEDIA, hashedBackupId) .addRemoveExpression(Map.entry("#expiredPrefixes", ATTR_EXPIRED_PREFIX)) .updateItemBuilder() .build()) @@ -539,17 +540,17 @@ private static class UpdateBuilder { private final Map attrNames = new HashMap<>(); private final String tableName; - private final BackupTier backupTier; + private final BackupLevel backupLevel; private final byte[] hashedBackupId; private String conditionExpression = null; static UpdateBuilder forUser(String tableName, AuthenticatedBackupUser backupUser) { - return new UpdateBuilder(tableName, backupUser.backupTier(), hashedBackupId(backupUser)); + return new UpdateBuilder(tableName, backupUser.backupLevel(), hashedBackupId(backupUser)); } - UpdateBuilder(String tableName, BackupTier backupTier, byte[] hashedBackupId) { + UpdateBuilder(String tableName, BackupLevel backupLevel, byte[] hashedBackupId) { this.tableName = tableName; - this.backupTier = backupTier; + this.backupLevel = backupLevel; this.hashedBackupId = hashedBackupId; } @@ -679,7 +680,7 @@ UpdateBuilder expireDirectoryNames( * Set the lastRefresh time as part of the update *

* This always updates lastRefreshTime, and updates lastMediaRefreshTime if the backup user has the appropriate - * tier. + * level. */ UpdateBuilder setRefreshTimes(final Clock clock) { return this.setRefreshTimes(clock.instant()); @@ -690,8 +691,8 @@ UpdateBuilder setRefreshTimes(final Instant refreshTime) { Map.entry("#lastRefreshTime", ATTR_LAST_REFRESH), Map.entry(":lastRefreshTime", AttributeValues.n(refreshTime.getEpochSecond()))); - if (backupTier.compareTo(BackupTier.MEDIA) >= 0) { - // update the media time if we have the appropriate tier + if (backupLevel.compareTo(BackupLevel.MEDIA) >= 0) { + // update the media time if we have the appropriate level addSetExpression("#lastMediaRefreshTime = :lastMediaRefreshTime", Map.entry("#lastMediaRefreshTime", ATTR_LAST_MEDIA_REFRESH), Map.entry(":lastMediaRefreshTime", AttributeValues.n(refreshTime.getEpochSecond()))); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/backup/ExpiredBackup.java b/service/src/main/java/org/whispersystems/textsecuregcm/backup/ExpiredBackup.java index 78ad972a8..615121143 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/backup/ExpiredBackup.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/backup/ExpiredBackup.java @@ -5,7 +5,6 @@ package org.whispersystems.textsecuregcm.backup; import java.time.Instant; -import java.util.List; /** * Represents a backup that requires some or all of its content to be deleted diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java index b3ccb3a69..a1bf5be7b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/SubscriptionConfiguration.java @@ -10,17 +10,18 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.Sets; import io.dropwizard.validation.ValidationMethod; +import org.whispersystems.textsecuregcm.backup.BackupLevelUtil; import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; import javax.validation.Valid; import javax.validation.constraints.Min; import javax.validation.constraints.NotNull; -import org.whispersystems.textsecuregcm.backup.BackupTier; public class SubscriptionConfiguration { @@ -78,11 +79,11 @@ public boolean areLevelConstraintsSatisfied() { // We have a tier for all configured backup levels final boolean backupLevelsMatch = backupLevels.keySet() .stream() - .allMatch(level -> BackupTier.fromReceiptLevel(level).orElse(BackupTier.NONE) != BackupTier.NONE); + .allMatch(SubscriptionConfiguration::isValidBackupLevel); // None of the donation levels correspond to backup levels final boolean donationLevelsDontMatch = donationLevels.keySet().stream() - .allMatch(level -> BackupTier.fromReceiptLevel(level).orElse(BackupTier.NONE) == BackupTier.NONE); + .allMatch(Predicate.not(SubscriptionConfiguration::isValidBackupLevel)); // The configured donation and backup levels don't intersect final boolean levelsDontIntersect = Sets.intersection(backupLevels.keySet(), donationLevels.keySet()).isEmpty(); @@ -105,4 +106,13 @@ public boolean isCurrencyListSameAcrossAllLevels() { Set currencies = any.get().prices().keySet(); return subscriptionLevels.values().stream().allMatch(level -> currencies.equals(level.prices().keySet())); } + + private static boolean isValidBackupLevel(final long receiptLevel) { + try { + BackupLevelUtil.fromReceiptLevel(receiptLevel); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthManagerTest.java index 5769f1780..a48fcf6b4 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthManagerTest.java @@ -37,6 +37,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.signal.libsignal.zkgroup.InvalidInputException; @@ -44,6 +45,7 @@ import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest; import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequestContext; +import org.signal.libsignal.zkgroup.backups.BackupLevel; import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; import org.signal.libsignal.zkgroup.receipts.ReceiptCredential; import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; @@ -61,6 +63,7 @@ import org.whispersystems.textsecuregcm.util.CompletableFutureTestUtil; import org.whispersystems.textsecuregcm.util.TestClock; import org.whispersystems.textsecuregcm.util.TestRandomUtil; +import javax.annotation.Nullable; public class BackupAuthManagerTest { @@ -79,9 +82,9 @@ void setUp() { reset(redeemedReceiptsManager); } - BackupAuthManager create(BackupTier backupTier, boolean rateLimit) { + BackupAuthManager create(@Nullable BackupLevel backupLevel, boolean rateLimit) { return new BackupAuthManager( - ExperimentHelper.withEnrollment(experimentName(backupTier), aci), + ExperimentHelper.withEnrollment(experimentName(backupLevel), aci), rateLimit ? denyRateLimiter(aci) : allowRateLimiter(), accountsManager, new ServerZkReceiptOperations(receiptParams), @@ -92,15 +95,16 @@ BackupAuthManager create(BackupTier backupTier, boolean rateLimit) { @ParameterizedTest @EnumSource - void commitRequiresBackupTier(final BackupTier backupTier) { - final BackupAuthManager authManager = create(backupTier, false); + @NullSource + void commitRequiresBackupLevel(final BackupLevel backupLevel) { + final BackupAuthManager authManager = create(backupLevel, false); final Account account = mock(Account.class); when(account.getUuid()).thenReturn(aci); when(accountsManager.updateAsync(any(), any())).thenReturn(CompletableFuture.completedFuture(account)); final ThrowableAssert.ThrowingCallable commit = () -> authManager.commitBackupId(account, backupAuthTestUtil.getRequest(backupKey, aci)).join(); - if (backupTier == BackupTier.NONE) { + if (backupLevel == null) { Assertions.assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(commit) .extracting(ex -> ex.getStatus().getCode()) @@ -113,8 +117,9 @@ void commitRequiresBackupTier(final BackupTier backupTier) { @ParameterizedTest @EnumSource - void credentialsRequiresBackupTier(final BackupTier backupTier) { - final BackupAuthManager authManager = create(backupTier, false); + @NullSource + void credentialsRequiresBackupLevel(final BackupLevel backupLevel) { + final BackupAuthManager authManager = create(backupLevel, false); final Account account = mock(Account.class); when(account.getUuid()).thenReturn(aci); @@ -125,7 +130,7 @@ void credentialsRequiresBackupTier(final BackupTier backupTier) { clock.instant().truncatedTo(ChronoUnit.DAYS), clock.instant().plus(Duration.ofDays(1)).truncatedTo(ChronoUnit.DAYS)).join()) .hasSize(2); - if (backupTier == BackupTier.NONE) { + if (backupLevel == null) { Assertions.assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(getCreds) .extracting(ex -> ex.getStatus().getCode()) @@ -136,9 +141,9 @@ void credentialsRequiresBackupTier(final BackupTier backupTier) { } @ParameterizedTest - @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = {"NONE"}) - void getReceiptCredentials(final BackupTier backupTier) throws VerificationFailedException { - final BackupAuthManager authManager = create(backupTier, false); + @EnumSource + void getReceiptCredentials(final BackupLevel backupLevel) throws VerificationFailedException { + final BackupAuthManager authManager = create(backupLevel, false); final BackupAuthCredentialRequestContext requestContext = BackupAuthCredentialRequestContext.create(backupKey, aci); @@ -153,10 +158,11 @@ void getReceiptCredentials(final BackupTier backupTier) throws VerificationFaile assertThat(creds).hasSize(8); Instant redemptionTime = start; for (BackupAuthManager.Credential cred : creds) { - requestContext.receiveResponse(cred.credential(), backupAuthTestUtil.params.getPublicParams(), - backupTier.getReceiptLevel()); - assertThat(cred.redemptionTime().getEpochSecond()) - .isEqualTo(redemptionTime.getEpochSecond()); + assertThat(requestContext + .receiveResponse(cred.credential(), redemptionTime, backupAuthTestUtil.params.getPublicParams()) + .getBackupLevel()) + .isEqualTo(backupLevel); + assertThat(cred.redemptionTime().getEpochSecond()).isEqualTo(redemptionTime.getEpochSecond()); redemptionTime = redemptionTime.plus(Duration.ofDays(1)); } } @@ -185,7 +191,7 @@ static Stream invalidCredentialTimeWindows() { @MethodSource void invalidCredentialTimeWindows(final Instant requestRedemptionStart, final Instant requestRedemptionEnd, final Instant now) { - final BackupAuthManager authManager = create(BackupTier.MESSAGES, false); + final BackupAuthManager authManager = create(BackupLevel.MESSAGES, false); final Account account = mock(Account.class); when(account.getUuid()).thenReturn(aci); @@ -206,21 +212,24 @@ void expiringBackupPayment() throws VerificationFailedException { final Instant day4 = Instant.EPOCH.plus(Duration.ofDays(4)); final Instant dayMax = day0.plus(BackupAuthManager.MAX_REDEMPTION_DURATION); - final BackupAuthManager authManager = create(BackupTier.MESSAGES, false); + final BackupAuthManager authManager = create(BackupLevel.MESSAGES, false); final Account account = mock(Account.class); when(account.getUuid()).thenReturn(aci); when(account.getBackupCredentialRequest()).thenReturn(backupAuthTestUtil.getRequest(backupKey, aci).serialize()); - when(account.getBackupVoucher()).thenReturn(new Account.BackupVoucher(BackupTier.MEDIA.getReceiptLevel(), day4)); + when(account.getBackupVoucher()).thenReturn(new Account.BackupVoucher(201, day4)); final List creds = authManager.getBackupAuthCredentials(account, day0, dayMax).join(); Instant redemptionTime = day0; final BackupAuthCredentialRequestContext requestContext = BackupAuthCredentialRequestContext.create(backupKey, aci); for (int i = 0; i < creds.size(); i++) { // Before the expiration, credentials should have a media receipt, otherwise messages only - final long level = i < 5 ? BackupTier.MEDIA.getReceiptLevel() : BackupTier.MESSAGES.getReceiptLevel(); + final BackupLevel level = i < 5 ? BackupLevel.MEDIA : BackupLevel.MESSAGES; final BackupAuthManager.Credential cred = creds.get(i); - requestContext.receiveResponse(cred.credential(), backupAuthTestUtil.params.getPublicParams(), level); + assertThat(requestContext + .receiveResponse(cred.credential(), redemptionTime, backupAuthTestUtil.params.getPublicParams()) + .getBackupLevel()) + .isEqualTo(level); assertThat(cred.redemptionTime().getEpochSecond()).isEqualTo(redemptionTime.getEpochSecond()); redemptionTime = redemptionTime.plus(Duration.ofDays(1)); } @@ -232,7 +241,7 @@ void expiredBackupPayment() { final Instant day2 = Instant.EPOCH.plus(Duration.ofDays(2)); final Instant day3 = Instant.EPOCH.plus(Duration.ofDays(3)); - final BackupAuthManager authManager = create(BackupTier.MESSAGES, false); + final BackupAuthManager authManager = create(BackupLevel.MESSAGES, false); final Account account = mock(Account.class); when(account.getUuid()).thenReturn(aci); when(account.getBackupVoucher()).thenReturn(new Account.BackupVoucher(3, day1)); @@ -247,8 +256,8 @@ void expiredBackupPayment() { assertThat(authManager.getBackupAuthCredentials(account, day2, day2.plus(Duration.ofDays(7))).join()) .hasSize(8); - @SuppressWarnings("unchecked") - final ArgumentCaptor> accountUpdater = ArgumentCaptor.forClass(Consumer.class); + @SuppressWarnings("unchecked") final ArgumentCaptor> accountUpdater = ArgumentCaptor.forClass( + Consumer.class); verify(accountsManager, times(1)).updateAsync(any(), accountUpdater.capture()); // If the account is not expired when we go to update it, we shouldn't wipe it out @@ -268,7 +277,7 @@ void expiredBackupPayment() { @Test void redeemReceipt() throws InvalidInputException, VerificationFailedException { final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1)); - final BackupAuthManager authManager = create(BackupTier.MESSAGES, false); + final BackupAuthManager authManager = create(BackupLevel.MESSAGES, false); final Account account = mock(Account.class); when(account.getUuid()).thenReturn(aci); @@ -285,8 +294,7 @@ void mergeRedemptions() throws InvalidInputException, VerificationFailedExceptio final Instant newExpirationTime = Instant.EPOCH.plus(Duration.ofDays(1)); final Instant existingExpirationTime = Instant.EPOCH.plus(Duration.ofDays(1)).plus(Duration.ofSeconds(1)); - - final BackupAuthManager authManager = create(BackupTier.MESSAGES, false); + final BackupAuthManager authManager = create(BackupLevel.MESSAGES, false); final Account account = mock(Account.class); when(account.getUuid()).thenReturn(aci); @@ -311,7 +319,7 @@ void mergeRedemptions() throws InvalidInputException, VerificationFailedExceptio void redeemExpiredReceipt() { final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1)); clock.pin(expirationTime.plus(Duration.ofSeconds(1))); - final BackupAuthManager authManager = create(BackupTier.MESSAGES, false); + final BackupAuthManager authManager = create(BackupLevel.MESSAGES, false); Assertions.assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), receiptPresentation(3, expirationTime)).join()) .extracting(ex -> ex.getStatus().getCode()) @@ -325,7 +333,7 @@ void redeemExpiredReceipt() { void redeemInvalidLevel(long level) { final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1)); clock.pin(expirationTime.plus(Duration.ofSeconds(1))); - final BackupAuthManager authManager = create(BackupTier.MESSAGES, false); + final BackupAuthManager authManager = create(BackupLevel.MESSAGES, false); Assertions.assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), receiptPresentation(level, expirationTime)).join()) @@ -337,7 +345,7 @@ void redeemInvalidLevel(long level) { @Test void redeemInvalidPresentation() throws InvalidInputException, VerificationFailedException { - final BackupAuthManager authManager = create(BackupTier.MESSAGES, false); + final BackupAuthManager authManager = create(BackupLevel.MESSAGES, false); final ReceiptCredentialPresentation invalid = receiptPresentation(ServerSecretParams.generate(), 3L, Instant.EPOCH); Assertions.assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), invalid).join()) @@ -350,7 +358,7 @@ void redeemInvalidPresentation() throws InvalidInputException, VerificationFaile @Test void receiptAlreadyRedeemed() throws InvalidInputException, VerificationFailedException { final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1)); - final BackupAuthManager authManager = create(BackupTier.MESSAGES, false); + final BackupAuthManager authManager = create(BackupLevel.MESSAGES, false); final Account account = mock(Account.class); when(account.getUuid()).thenReturn(aci); @@ -390,7 +398,7 @@ private ReceiptCredentialPresentation receiptPresentation(ServerSecretParams par @Test void testRateLimits() { final AccountsManager accountsManager = mock(AccountsManager.class); - final BackupAuthManager authManager = create(BackupTier.MESSAGES, true); + final BackupAuthManager authManager = create(BackupLevel.MESSAGES, true); final BackupAuthCredentialRequest credentialRequest = backupAuthTestUtil.getRequest(backupKey, aci); @@ -407,11 +415,11 @@ void testRateLimits() { assertDoesNotThrow(() -> authManager.commitBackupId(account, credentialRequest).join()); } - private static String experimentName(BackupTier backupTier) { - return switch (backupTier) { + private static String experimentName(@Nullable BackupLevel backupLevel) { + return switch (backupLevel) { case MESSAGES -> BackupAuthManager.BACKUP_EXPERIMENT_NAME; case MEDIA -> BackupAuthManager.BACKUP_MEDIA_EXPERIMENT_NAME; - case NONE -> "fake_experiment"; + case null -> "fake_experiment"; }; } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthTestUtil.java b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthTestUtil.java index ddd8b70cd..e52e01ce2 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthTestUtil.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupAuthTestUtil.java @@ -18,6 +18,7 @@ import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation; import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest; import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequestContext; +import org.signal.libsignal.zkgroup.backups.BackupLevel; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.tests.util.ExperimentHelper; @@ -35,32 +36,32 @@ public BackupAuthCredentialRequest getRequest(final byte[] backupKey, final UUID } public BackupAuthCredentialPresentation getPresentation( - final BackupTier backupTier, final byte[] backupKey, final UUID aci) + final BackupLevel backupLevel, final byte[] backupKey, final UUID aci) throws VerificationFailedException { - return getPresentation(params, backupTier, backupKey, aci); + return getPresentation(params, backupLevel, backupKey, aci); } public BackupAuthCredentialPresentation getPresentation( - GenericServerSecretParams params, final BackupTier backupTier, final byte[] backupKey, final UUID aci) + GenericServerSecretParams params, final BackupLevel backupLevel, final byte[] backupKey, final UUID aci) throws VerificationFailedException { + final Instant redemptionTime = clock.instant().truncatedTo(ChronoUnit.DAYS); final BackupAuthCredentialRequestContext ctx = BackupAuthCredentialRequestContext.create(backupKey, aci); return ctx.receiveResponse( ctx.getRequest() - .issueCredential(clock.instant().truncatedTo(ChronoUnit.DAYS), backupTier.getReceiptLevel(), params), - params.getPublicParams(), - backupTier.getReceiptLevel()) + .issueCredential(clock.instant().truncatedTo(ChronoUnit.DAYS), backupLevel, params), + redemptionTime, + params.getPublicParams()) .present(params.getPublicParams()); } public List getCredentials( - final BackupTier backupTier, + final BackupLevel backupLevel, final BackupAuthCredentialRequest request, final Instant redemptionStart, final Instant redemptionEnd) { final UUID aci = UUID.randomUUID(); - final String experimentName = switch (backupTier) { - case NONE -> "notUsed"; + final String experimentName = switch (backupLevel) { case MESSAGES -> BackupAuthManager.BACKUP_EXPERIMENT_NAME; case MEDIA -> BackupAuthManager.BACKUP_MEDIA_EXPERIMENT_NAME; }; 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 2576113a3..f9909a5ec 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupManagerTest.java @@ -30,7 +30,6 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -61,6 +60,7 @@ import org.signal.libsignal.zkgroup.GenericServerSecretParams; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation; +import org.signal.libsignal.zkgroup.backups.BackupLevel; import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator; import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; @@ -90,7 +90,6 @@ public class BackupManagerTest { private final RemoteStorageManager remoteStorageManager = mock(RemoteStorageManager.class); private final byte[] backupKey = TestRandomUtil.nextBytes(32); private final UUID aci = UUID.randomUUID(); - private final SecureRandom secureRandom = new SecureRandom(); private BackupManager backupManager; private BackupsDb backupsDb; @@ -119,13 +118,13 @@ public void setup() { } @ParameterizedTest - @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = {"NONE"}) - public void createBackup(final BackupTier backupTier) { + @EnumSource + public void createBackup(final BackupLevel backupLevel) { final Instant now = Instant.ofEpochSecond(Duration.ofDays(1).getSeconds()); testClock.pin(now); - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), backupTier); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), backupLevel); backupManager.createMessageBackupUploadDescriptor(backupUser).join(); verify(tusCredentialGenerator, times(1)) @@ -137,12 +136,12 @@ public void createBackup(final BackupTier backupTier) { assertThat(info.mediaUsedSpace()).isEqualTo(Optional.empty()); // Check that the initial expiration times are the initial write times - checkExpectedExpirations(now, backupTier == BackupTier.MEDIA ? now : null, backupUser); + checkExpectedExpirations(now, backupLevel == BackupLevel.MEDIA ? now : null, backupUser); } @Test public void createTemporaryMediaAttachmentRateLimited() throws RateLimitExceededException { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); doThrow(new RateLimitExceededException(null, true)) .when(mediaUploadLimiter) .validate(eq(BackupManager.rateLimitKey(backupUser))); @@ -153,8 +152,8 @@ public void createTemporaryMediaAttachmentRateLimited() throws RateLimitExceeded } @Test - public void createTemporaryMediaAttachmentWrongTier() throws RateLimitExceededException { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MESSAGES); + public void createTemporaryMediaAttachmentWrongTier() { + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MESSAGES); assertThatExceptionOfType(StatusRuntimeException.class) .isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser)) .extracting(StatusRuntimeException::getStatus) @@ -163,9 +162,9 @@ public void createTemporaryMediaAttachmentWrongTier() throws RateLimitExceededEx } @ParameterizedTest - @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = {"NONE"}) - public void ttlRefresh(final BackupTier backupTier) { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), backupTier); + @EnumSource + public void ttlRefresh(final BackupLevel backupLevel) { + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), backupLevel); final Instant tstart = Instant.ofEpochSecond(1).plus(Duration.ofDays(1)); final Instant tnext = tstart.plus(Duration.ofSeconds(1)); @@ -180,17 +179,17 @@ public void ttlRefresh(final BackupTier backupTier) { checkExpectedExpirations( tnext, - backupTier == BackupTier.MEDIA ? tnext : null, + backupLevel == BackupLevel.MEDIA ? tnext : null, backupUser); } @ParameterizedTest - @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = {"NONE"}) - public void createBackupRefreshesTtl(final BackupTier backupTier) { + @EnumSource + public void createBackupRefreshesTtl(final BackupLevel backupLevel) { final Instant tstart = Instant.ofEpochSecond(1).plus(Duration.ofDays(1)); final Instant tnext = tstart.plus(Duration.ofSeconds(1)); - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), backupTier); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), backupLevel); // create backup at t=tstart testClock.pin(tstart); @@ -202,7 +201,7 @@ public void createBackupRefreshesTtl(final BackupTier backupTier) { checkExpectedExpirations( tnext, - backupTier == BackupTier.MEDIA ? tnext : null, + backupLevel == BackupLevel.MEDIA ? tnext : null, backupUser); } @@ -210,7 +209,7 @@ public void createBackupRefreshesTtl(final BackupTier backupTier) { public void invalidPresentationNoPublicKey() throws VerificationFailedException { final BackupAuthCredentialPresentation invalidPresentation = backupAuthTestUtil.getPresentation( GenericServerSecretParams.generate(), - BackupTier.MESSAGES, backupKey, aci); + BackupLevel.MESSAGES, backupKey, aci); final ECKeyPair keyPair = Curve.generateKeyPair(); @@ -228,10 +227,10 @@ public void invalidPresentationNoPublicKey() throws VerificationFailedException @Test public void invalidPresentationCorrectSignature() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MESSAGES, backupKey, aci); + BackupLevel.MESSAGES, backupKey, aci); final BackupAuthCredentialPresentation invalidPresentation = backupAuthTestUtil.getPresentation( GenericServerSecretParams.generate(), - BackupTier.MESSAGES, backupKey, aci); + BackupLevel.MESSAGES, backupKey, aci); final ECKeyPair keyPair = Curve.generateKeyPair(); backupManager.setPublicKey( @@ -251,7 +250,7 @@ public void invalidPresentationCorrectSignature() throws VerificationFailedExcep @Test public void unknownPublicKey() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MESSAGES, backupKey, aci); + BackupLevel.MESSAGES, backupKey, aci); final ECKeyPair keyPair = Curve.generateKeyPair(); final byte[] signature = keyPair.getPrivateKey().calculateSignature(presentation.serialize()); @@ -267,7 +266,7 @@ public void unknownPublicKey() throws VerificationFailedException { @Test public void mismatchedPublicKey() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MESSAGES, backupKey, aci); + BackupLevel.MESSAGES, backupKey, aci); final ECKeyPair keyPair1 = Curve.generateKeyPair(); final ECKeyPair keyPair2 = Curve.generateKeyPair(); @@ -290,7 +289,7 @@ public void mismatchedPublicKey() throws VerificationFailedException { @Test public void signatureValidation() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MESSAGES, backupKey, aci); + BackupLevel.MESSAGES, backupKey, aci); final ECKeyPair keyPair = Curve.generateKeyPair(); final byte[] signature = keyPair.getPrivateKey().calculateSignature(presentation.serialize()); @@ -317,7 +316,7 @@ public void signatureValidation() throws VerificationFailedException { // correct signature final AuthenticatedBackupUser user = backupManager.authenticateBackupUser(presentation, signature).join(); assertThat(user.backupId()).isEqualTo(presentation.getBackupId()); - assertThat(user.backupTier()).isEqualTo(BackupTier.MESSAGES); + assertThat(user.backupLevel()).isEqualTo(BackupLevel.MESSAGES); } @Test @@ -325,7 +324,7 @@ public void credentialExpiration() throws VerificationFailedException { // credential for 1 day after epoch testClock.pin(Instant.ofEpochSecond(1).plus(Duration.ofDays(1))); - final BackupAuthCredentialPresentation oldCredential = backupAuthTestUtil.getPresentation(BackupTier.MESSAGES, + final BackupAuthCredentialPresentation oldCredential = backupAuthTestUtil.getPresentation(BackupLevel.MESSAGES, backupKey, aci); final ECKeyPair keyPair = Curve.generateKeyPair(); final byte[] signature = keyPair.getPrivateKey().calculateSignature(oldCredential.serialize()); @@ -350,7 +349,7 @@ public void credentialExpiration() throws VerificationFailedException { @Test public void copySuccess() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); when(tusCredentialGenerator.generateUpload(any())) .thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), "")); when(remoteStorageManager.copy(eq(URI.create("cdn3.example.org/attachments/abc")), eq(100), any(), any())) @@ -376,7 +375,7 @@ public void copySuccess() { @Test public void copyFailure() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); when(tusCredentialGenerator.generateUpload(any())) .thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), "")); when(remoteStorageManager.copy(eq(URI.create("cdn3.example.org/attachments/abc")), eq(100), any(), any())) @@ -397,7 +396,7 @@ public void copyFailure() { @Test public void unknownSourceCdn() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); CompletableFutureTestUtil.assertFailsWithCause(SourceObjectNotFoundException.class, backupManager.copyToBackup( backupUser, @@ -408,7 +407,7 @@ public void unknownSourceCdn() { @Test public void quotaEnforcementNoRecalculation() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); verifyNoInteractions(remoteStorageManager); // set the backupsDb to be out of quota at t=0 @@ -423,7 +422,7 @@ public void quotaEnforcementNoRecalculation() { @Test public void quotaEnforcementRecalculation() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); final String backupMediaPrefix = "%s/%s/".formatted(backupUser.backupDir(), backupUser.mediaDir()); // on recalculation, say there's actually 10 bytes left @@ -460,7 +459,7 @@ public void quotaEnforcement( final long spaceLeft, final long mediaToAddSize, boolean shouldAccept) { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); final String backupMediaPrefix = "%s/%s/".formatted(backupUser.backupDir(), backupUser.mediaDir()); // set the backupsDb to be out of quota at t=0 @@ -485,7 +484,7 @@ public void quotaEnforcement( @ValueSource(strings = {"", "cursor"}) public void list(final String cursorVal) { final Optional cursor = Optional.of(cursorVal).filter(StringUtils::isNotBlank); - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); final String backupMediaPrefix = "%s/%s/".formatted(backupUser.backupDir(), backupUser.mediaDir()); when(remoteStorageManager.cdnNumber()).thenReturn(13); @@ -498,24 +497,24 @@ public void list(final String cursorVal) { final BackupManager.ListMediaResult result = backupManager.list(backupUser, cursor, 17) .toCompletableFuture().join(); assertThat(result.media()).hasSize(1); - assertThat(result.media().get(0).cdn()).isEqualTo(13); - assertThat(result.media().get(0).key()).isEqualTo( + assertThat(result.media().getFirst().cdn()).isEqualTo(13); + assertThat(result.media().getFirst().key()).isEqualTo( Base64.getDecoder().decode("aaa".getBytes(StandardCharsets.UTF_8))); - assertThat(result.media().get(0).length()).isEqualTo(123); + assertThat(result.media().getFirst().length()).isEqualTo(123); assertThat(result.cursor().get()).isEqualTo("newCursor"); } @Test public void deleteEntireBackup() { - final AuthenticatedBackupUser original = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser original = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); testClock.pin(Instant.ofEpochSecond(10)); // Deleting should swap the backupDir for the user backupManager.deleteEntireBackup(original).join(); verifyNoInteractions(remoteStorageManager); - final AuthenticatedBackupUser after = retrieveBackupUser(original.backupId(), BackupTier.MEDIA); + final AuthenticatedBackupUser after = retrieveBackupUser(original.backupId(), BackupLevel.MEDIA); assertThat(original.backupDir()).isNotEqualTo(after.backupDir()); assertThat(original.mediaDir()).isNotEqualTo(after.mediaDir()); @@ -541,7 +540,7 @@ public void deleteEntireBackup() { @Test public void delete() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); final byte[] mediaId = TestRandomUtil.nextBytes(16); final String backupMediaKey = "%s/%s/%s".formatted( backupUser.backupDir(), @@ -562,7 +561,7 @@ public void delete() { @Test public void deleteUnknownCdn() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); when(remoteStorageManager.cdnNumber()).thenReturn(5); assertThatThrownBy(() -> backupManager.delete(backupUser, List.of(new BackupManager.StorageDescriptor(4, TestRandomUtil.nextBytes(15))))) @@ -572,7 +571,7 @@ public void deleteUnknownCdn() { @Test public void deletePartialFailure() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); final List descriptors = new ArrayList<>(); long initialBytes = 0; @@ -605,7 +604,7 @@ public void deletePartialFailure() { @Test public void alreadyDeleted() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); final byte[] mediaId = TestRandomUtil.nextBytes(16); final String backupMediaKey = "%s/%s/%s".formatted( backupUser.backupDir(), @@ -627,7 +626,7 @@ public void alreadyDeleted() { @Test public void listExpiredBackups() { final List backupUsers = IntStream.range(0, 10) - .mapToObj(i -> backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA)) + .mapToObj(i -> backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA)) .toList(); for (int i = 0; i < backupUsers.size(); i++) { testClock.pin(Instant.ofEpochSecond(i)); @@ -665,11 +664,11 @@ public void listExpiredBackupsByTier() { // refreshed media timestamp at t=5 testClock.pin(Instant.ofEpochSecond(5)); - backupManager.createMessageBackupUploadDescriptor(backupUser(backupId, BackupTier.MEDIA)).join(); + backupManager.createMessageBackupUploadDescriptor(backupUser(backupId, BackupLevel.MEDIA)).join(); // refreshed messages timestamp at t=6 testClock.pin(Instant.ofEpochSecond(6)); - backupManager.createMessageBackupUploadDescriptor(backupUser(backupId, BackupTier.MESSAGES)).join(); + backupManager.createMessageBackupUploadDescriptor(backupUser(backupId, BackupLevel.MESSAGES)).join(); Function> getExpired = time -> backupManager .getExpiredBackups(1, Schedulers.immediate(), time) @@ -689,7 +688,7 @@ public void listExpiredBackupsByTier() { @ParameterizedTest @EnumSource(mode = EnumSource.Mode.INCLUDE, names = {"MEDIA", "ALL"}) public void expireBackup(ExpiredBackup.ExpirationType expirationType) { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); backupManager.createMessageBackupUploadDescriptor(backupUser).join(); final String expectedPrefixToDelete = switch (expirationType) { @@ -731,7 +730,7 @@ public void expireBackup(ExpiredBackup.ExpirationType expirationType) { @Test public void deleteBackupPaginated() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); backupManager.createMessageBackupUploadDescriptor(backupUser).join(); final ExpiredBackup expiredBackup = expiredBackup(ExpiredBackup.ExpirationType.MEDIA, backupUser); @@ -814,23 +813,23 @@ private static byte[] hashedBackupId(final byte[] backupId) { /** * Create BackupUser with the provided backupId and tier */ - private AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupTier backupTier) { + private AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupLevel backupLevel) { // Won't actually validate the public key, but need to have a public key to perform BackupsDB operations byte[] privateKey = new byte[32]; ByteBuffer.wrap(privateKey).put(backupId); try { - backupsDb.setPublicKey(backupId, backupTier, Curve.decodePrivatePoint(privateKey).publicKey()).join(); + backupsDb.setPublicKey(backupId, backupLevel, Curve.decodePrivatePoint(privateKey).publicKey()).join(); } catch (InvalidKeyException e) { throw new RuntimeException(e); } - return retrieveBackupUser(backupId, backupTier); + return retrieveBackupUser(backupId, backupLevel); } /** * Retrieve an existing BackupUser from the database */ - private AuthenticatedBackupUser retrieveBackupUser(final byte[] backupId, final BackupTier backupTier) { + private AuthenticatedBackupUser retrieveBackupUser(final byte[] backupId, final BackupLevel backupLevel) { final BackupsDb.AuthenticationData authData = backupsDb.retrieveAuthenticationData(backupId).join().get(); - return new AuthenticatedBackupUser(backupId, backupTier, authData.backupDir(), authData.mediaDir()); + return new AuthenticatedBackupUser(backupId, backupLevel, authData.backupDir(), authData.mediaDir()); } } 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 b26fa2453..f42194f44 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/BackupsDbTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import org.signal.libsignal.protocol.ecc.Curve; +import org.signal.libsignal.zkgroup.backups.BackupLevel; import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser; import org.whispersystems.textsecuregcm.storage.DynamoDbExtension; import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema; @@ -48,7 +49,7 @@ public void setup() { @Test public void trackMediaStats() { - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); // add at least one message backup so we can describe it backupsDb.addMessageBackup(backupUser).join(); int total = 0; @@ -71,7 +72,7 @@ public void trackMediaStats() { @ValueSource(booleans = {false, true}) public void setUsage(boolean mediaAlreadyExists) { testClock.pin(Instant.ofEpochSecond(5)); - final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA); + final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupLevel.MEDIA); if (mediaAlreadyExists) { this.backupsDb.trackMedia(backupUser, 1, 10).join(); } @@ -87,12 +88,12 @@ public void expirationDetectedOnce() { final byte[] backupId = TestRandomUtil.nextBytes(16); // Refresh media/messages at t=0 testClock.pin(Instant.ofEpochSecond(0L)); - backupsDb.setPublicKey(backupId, BackupTier.MEDIA, Curve.generateKeyPair().getPublicKey()).join(); - this.backupsDb.ttlRefresh(backupUser(backupId, BackupTier.MEDIA)).join(); + backupsDb.setPublicKey(backupId, BackupLevel.MEDIA, Curve.generateKeyPair().getPublicKey()).join(); + this.backupsDb.ttlRefresh(backupUser(backupId, BackupLevel.MEDIA)).join(); // refresh only messages at t=2 testClock.pin(Instant.ofEpochSecond(2L)); - this.backupsDb.ttlRefresh(backupUser(backupId, BackupTier.MESSAGES)).join(); + this.backupsDb.ttlRefresh(backupUser(backupId, BackupLevel.MESSAGES)).join(); final Function> expiredBackups = purgeTime -> backupsDb .getExpiredBackups(1, Schedulers.immediate(), purgeTime) @@ -104,8 +105,8 @@ public void expirationDetectedOnce() { .matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.MEDIA); // Expire the media - backupsDb.startExpiration(expired.get(0)).join(); - backupsDb.finishExpiration(expired.get(0)).join(); + backupsDb.startExpiration(expired.getFirst()).join(); + backupsDb.finishExpiration(expired.getFirst()).join(); // should be nothing to expire at t=1 assertThat(expiredBackups.apply(Instant.ofEpochSecond(1))).isEmpty(); @@ -116,8 +117,8 @@ public void expirationDetectedOnce() { .matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.ALL); // Expire the messages - backupsDb.startExpiration(expired.get(0)).join(); - backupsDb.finishExpiration(expired.get(0)).join(); + backupsDb.startExpiration(expired.getFirst()).join(); + backupsDb.finishExpiration(expired.getFirst()).join(); // should be nothing to expire at t=3 assertThat(expiredBackups.apply(Instant.ofEpochSecond(3))).isEmpty(); @@ -129,13 +130,13 @@ public void expirationFailed(ExpiredBackup.ExpirationType expirationType) { final byte[] backupId = TestRandomUtil.nextBytes(16); // Refresh media/messages at t=0 testClock.pin(Instant.ofEpochSecond(0L)); - backupsDb.setPublicKey(backupId, BackupTier.MEDIA, Curve.generateKeyPair().getPublicKey()).join(); - this.backupsDb.ttlRefresh(backupUser(backupId, BackupTier.MEDIA)).join(); + backupsDb.setPublicKey(backupId, BackupLevel.MEDIA, Curve.generateKeyPair().getPublicKey()).join(); + this.backupsDb.ttlRefresh(backupUser(backupId, BackupLevel.MEDIA)).join(); if (expirationType == ExpiredBackup.ExpirationType.MEDIA) { // refresh only messages at t=2 so that we only expire media at t=1 testClock.pin(Instant.ofEpochSecond(2L)); - this.backupsDb.ttlRefresh(backupUser(backupId, BackupTier.MESSAGES)).join(); + this.backupsDb.ttlRefresh(backupUser(backupId, BackupLevel.MESSAGES)).join(); } final Function> expiredBackups = purgeTime -> { @@ -160,17 +161,17 @@ public void expirationFailed(ExpiredBackup.ExpirationType expirationType) { if (expirationType == ExpiredBackup.ExpirationType.MEDIA) { // Media expiration should swap the media name and keep the backup name, marking the old media name for expiration assertThat(expired.prefixToDelete()) - .isEqualTo(originalBackupDir + "/" + originalMediaDir) - .withFailMessage("Should expire media directory, expired %s", expired.prefixToDelete()); - assertThat(info.backupDir()).isEqualTo(originalBackupDir).withFailMessage("should keep backupDir"); - assertThat(info.mediaDir()).isNotEqualTo(originalMediaDir).withFailMessage("should change mediaDir"); + .withFailMessage("Should expire media directory, expired %s", expired.prefixToDelete()) + .isEqualTo(originalBackupDir + "/" + originalMediaDir); + assertThat(info.backupDir()).withFailMessage("should keep backupDir").isEqualTo(originalBackupDir); + assertThat(info.mediaDir()).withFailMessage("should change mediaDir").isNotEqualTo(originalMediaDir); } else { // Full expiration should swap the media name and the backup name, marking the old backup name for expiration assertThat(expired.prefixToDelete()) - .isEqualTo(originalBackupDir) - .withFailMessage("Should expire whole backupDir, expired %s", expired.prefixToDelete()); - assertThat(info.backupDir()).isNotEqualTo(originalBackupDir).withFailMessage("should change backupDir"); - assertThat(info.mediaDir()).isNotEqualTo(originalMediaDir).withFailMessage("should change mediaDir"); + .withFailMessage("Should expire whole backupDir, expired %s", expired.prefixToDelete()) + .isEqualTo(originalBackupDir); + assertThat(info.backupDir()).withFailMessage("should change backupDir").isNotEqualTo(originalBackupDir); + assertThat(info.mediaDir()).withFailMessage("should change mediaDir").isNotEqualTo(originalMediaDir); } final String expiredPrefix = expired.prefixToDelete(); @@ -189,7 +190,7 @@ public void expirationFailed(ExpiredBackup.ExpirationType expirationType) { // should be nothing to expire at t=1 assertThat(opt).isEmpty(); // The backup should still exist - backupsDb.describeBackup(backupUser(backupId, BackupTier.MEDIA)).join(); + backupsDb.describeBackup(backupUser(backupId, BackupLevel.MEDIA)).join(); } else { // Cleaned up the failed attempt, now should tell us to clean the whole backup assertThat(opt.get()).matches(eb -> eb.expirationType() == ExpiredBackup.ExpirationType.ALL, @@ -199,20 +200,14 @@ public void expirationFailed(ExpiredBackup.ExpirationType expirationType) { // The backup entry should be gone assertThat(CompletableFutureTestUtil.assertFailsWithCause(StatusRuntimeException.class, - backupsDb.describeBackup(backupUser(backupId, BackupTier.MEDIA))) - .getStatus().getCode()) + backupsDb.describeBackup(backupUser(backupId, BackupLevel.MEDIA))) + .getStatus().getCode()) .isEqualTo(Status.Code.NOT_FOUND); assertThat(expiredBackups.apply(Instant.ofEpochSecond(10))).isEmpty(); } } - private AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupTier backupTier) { - return new AuthenticatedBackupUser(backupId, backupTier, "myBackupDir", "myMediaDir"); - } - - private AuthenticatedBackupUser backupUserFromDb(final byte[] backupId, final BackupTier backupTier) { - final BackupsDb.AuthenticationData authenticationData = backupsDb.retrieveAuthenticationData(backupId).join().get(); - return new AuthenticatedBackupUser(backupId, backupTier, - authenticationData.backupDir(), authenticationData.mediaDir()); + private AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupLevel backupLevel) { + return new AuthenticatedBackupUser(backupId, backupLevel, "myBackupDir", "myMediaDir"); } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/backup/Cdn3RemoteStorageManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/backup/Cdn3RemoteStorageManagerTest.java index 9920016e7..69d0c6431 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/backup/Cdn3RemoteStorageManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/backup/Cdn3RemoteStorageManagerTest.java @@ -51,18 +51,18 @@ @ExtendWith(DropwizardExtensionsSupport.class) public class Cdn3RemoteStorageManagerTest { - private static byte[] HMAC_KEY = TestRandomUtil.nextBytes(32); - private static byte[] AES_KEY = TestRandomUtil.nextBytes(32); - private static byte[] IV = TestRandomUtil.nextBytes(16); + private static final byte[] HMAC_KEY = TestRandomUtil.nextBytes(32); + private static final byte[] AES_KEY = TestRandomUtil.nextBytes(32); + private static final byte[] IV = TestRandomUtil.nextBytes(16); @RegisterExtension - private final WireMockExtension wireMock = WireMockExtension.newInstance() + private static final WireMockExtension wireMock = WireMockExtension.newInstance() .options(wireMockConfig().dynamicPort()) .build(); - private static String SMALL_CDN2 = "a small object from cdn2"; - private static String SMALL_CDN3 = "a small object from cdn3"; - private static String LARGE = "a".repeat(1024 * 1024 * 5); + private static final String SMALL_CDN2 = "a small object from cdn2"; + private static final String SMALL_CDN3 = "a small object from cdn3"; + private static final String LARGE = "a".repeat(1024 * 1024 * 5); private RemoteStorageManager remoteStorageManager; @@ -127,7 +127,7 @@ public void copySmall(final int sourceCdn) new BackupUploadDescriptor(3, "test", Collections.emptyMap(), wireMock.url("/cdn3/dest"))) .toCompletableFuture().join(); - final byte[] destBody = wireMock.findAll(postRequestedFor(urlEqualTo("/cdn3/dest"))).get(0).getBody(); + final byte[] destBody = wireMock.findAll(postRequestedFor(urlEqualTo("/cdn3/dest"))).getFirst().getBody(); assertThat(new String(decrypt(destBody), StandardCharsets.UTF_8)) .isEqualTo(expectedSource); } @@ -151,7 +151,7 @@ public void copyLarge() new BackupUploadDescriptor(3, "test", Collections.emptyMap(), wireMock.url("/cdn3/dest"))) .toCompletableFuture().join(); - final byte[] destBody = wireMock.findAll(postRequestedFor(urlEqualTo("/cdn3/dest"))).get(0).getBody(); + final byte[] destBody = wireMock.findAll(postRequestedFor(urlEqualTo("/cdn3/dest"))).getFirst().getBody(); assertThat(destBody.length) .isEqualTo(new BackupMediaEncrypter(params).outputSize(LARGE.length())) .isEqualTo(params.outputSize(LARGE.length())); 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 0bd06f561..1a3f0c792 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ArchiveControllerTest.java @@ -53,6 +53,7 @@ import org.signal.libsignal.zkgroup.ServerSecretParams; import org.signal.libsignal.zkgroup.VerificationFailedException; import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation; +import org.signal.libsignal.zkgroup.backups.BackupLevel; import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations; import org.signal.libsignal.zkgroup.receipts.ReceiptCredential; import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation; @@ -65,7 +66,6 @@ import org.whispersystems.textsecuregcm.backup.BackupAuthManager; import org.whispersystems.textsecuregcm.backup.BackupAuthTestUtil; import org.whispersystems.textsecuregcm.backup.BackupManager; -import org.whispersystems.textsecuregcm.backup.BackupTier; import org.whispersystems.textsecuregcm.backup.InvalidLengthException; import org.whispersystems.textsecuregcm.backup.SourceObjectNotFoundException; import org.whispersystems.textsecuregcm.backup.BackupUploadDescriptor; @@ -133,7 +133,7 @@ public void setUp() { public void anonymousAuthOnly(final String method, final String path, final String body) throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MEDIA, backupKey, aci); + BackupLevel.MEDIA, backupKey, aci); final Invocation.Builder request = resources.getJerseyTest() .target(path) .request() @@ -192,7 +192,7 @@ public void setBadPublicKey() throws VerificationFailedException { when(backupManager.setPublicKey(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null)); final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MEDIA, backupKey, aci); + BackupLevel.MEDIA, backupKey, aci); final Response response = resources.getJerseyTest() .target("v1/archives/keys") .request() @@ -209,7 +209,7 @@ public void setMissingPublicKey() throws VerificationFailedException { when(backupManager.setPublicKey(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null)); final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MEDIA, backupKey, aci); + BackupLevel.MEDIA, backupKey, aci); final Response response = resources.getJerseyTest() .target("v1/archives/keys") .request() @@ -224,7 +224,7 @@ public void setPublicKey() throws VerificationFailedException { when(backupManager.setPublicKey(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(null)); final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MEDIA, backupKey, aci); + BackupLevel.MEDIA, backupKey, aci); final Response response = resources.getJerseyTest() .target("v1/archives/keys") .request() @@ -283,7 +283,7 @@ public void getCredentials() { final Instant start = Instant.now().truncatedTo(ChronoUnit.DAYS); final Instant end = start.plus(Duration.ofDays(1)); final List expectedResponse = backupAuthTestUtil.getCredentials( - BackupTier.MEDIA, backupAuthTestUtil.getRequest(backupKey, aci), start, end); + BackupLevel.MEDIA, backupAuthTestUtil.getRequest(backupKey, aci), start, end); when(backupAuthManager.getBackupAuthCredentials(any(), eq(start), eq(end))).thenReturn( CompletableFuture.completedFuture(expectedResponse)); final ArchiveController.BackupAuthCredentialsResponse creds = resources.getJerseyTest() @@ -293,10 +293,10 @@ public void getCredentials() { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .get(ArchiveController.BackupAuthCredentialsResponse.class); - assertThat(creds.credentials().get(0).redemptionTime()).isEqualTo(start.getEpochSecond()); + assertThat(creds.credentials().getFirst().redemptionTime()).isEqualTo(start.getEpochSecond()); } - enum BadCredentialsType {MISSING_START, MISSING_END, MISSING_BOTH} + public enum BadCredentialsType {MISSING_START, MISSING_END, MISSING_BOTH} @ParameterizedTest @EnumSource @@ -323,9 +323,9 @@ public void getCredentialsBadInput(final BadCredentialsType badCredentialsType) @Test public void getBackupInfo() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MEDIA, backupKey, aci); + BackupLevel.MEDIA, backupKey, aci); when(backupManager.authenticateBackupUser(any(), any())) - .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupTier.MEDIA))); + .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupLevel.MEDIA))); when(backupManager.backupInfo(any())).thenReturn(CompletableFuture.completedFuture(new BackupManager.BackupInfo( 1, "myBackupDir", "myMediaDir", "filename", Optional.empty()))); final ArchiveController.BackupInfoResponse response = resources.getJerseyTest() @@ -343,9 +343,9 @@ public void getBackupInfo() throws VerificationFailedException { @Test public void putMediaBatchSuccess() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MEDIA, backupKey, aci); + BackupLevel.MEDIA, backupKey, aci); when(backupManager.authenticateBackupUser(any(), any())) - .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupTier.MEDIA))); + .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupLevel.MEDIA))); when(backupManager.canStoreMedia(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(true)); when(backupManager.copyToBackup(any(), anyInt(), any(), anyInt(), any(), any())) .thenAnswer(invocation -> { @@ -393,9 +393,9 @@ public void putMediaBatchSuccess() throws VerificationFailedException { public void putMediaBatchPartialFailure() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MEDIA, backupKey, aci); + BackupLevel.MEDIA, backupKey, aci); when(backupManager.authenticateBackupUser(any(), any())) - .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupTier.MEDIA))); + .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupLevel.MEDIA))); final byte[][] mediaIds = IntStream.range(0, 3).mapToObj(i -> TestRandomUtil.nextBytes(15)).toArray(byte[][]::new); when(backupManager.canStoreMedia(any(), anyLong())).thenReturn(CompletableFuture.completedFuture(true)); @@ -448,9 +448,9 @@ public void putMediaBatchPartialFailure() throws VerificationFailedException { @Test public void putMediaBatchOutOfSpace() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MEDIA, backupKey, aci); + BackupLevel.MEDIA, backupKey, aci); when(backupManager.authenticateBackupUser(any(), any())) - .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupTier.MEDIA))); + .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupLevel.MEDIA))); when(backupManager.canStoreMedia(any(), eq(1L + 2L + 3L))) .thenReturn(CompletableFuture.completedFuture(false)); @@ -478,9 +478,9 @@ public void list( @CartesianTest.Values(booleans = {true, false}) final boolean cursorReturned) throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation( - BackupTier.MEDIA, backupKey, aci); + BackupLevel.MEDIA, backupKey, aci); when(backupManager.authenticateBackupUser(any(), any())) - .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupTier.MEDIA))); + .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupLevel.MEDIA))); final byte[] mediaId = TestRandomUtil.nextBytes(15); final Optional expectedCursor = cursorProvided ? Optional.of("myCursor") : Optional.empty(); @@ -505,17 +505,17 @@ public void list( .get(ArchiveController.ListResponse.class); assertThat(response.storedMediaObjects()).hasSize(1); - assertThat(response.storedMediaObjects().get(0).objectLength()).isEqualTo(100); - assertThat(response.storedMediaObjects().get(0).mediaId()).isEqualTo(mediaId); + assertThat(response.storedMediaObjects().getFirst().objectLength()).isEqualTo(100); + assertThat(response.storedMediaObjects().getFirst().mediaId()).isEqualTo(mediaId); assertThat(response.cursor()).isEqualTo(returnedCursor.orElse(null)); } @Test public void delete() throws VerificationFailedException { - final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(BackupTier.MEDIA, + final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(BackupLevel.MEDIA, backupKey, aci); when(backupManager.authenticateBackupUser(any(), any())) - .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupTier.MEDIA))); + .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupLevel.MEDIA))); final ArchiveController.DeleteMedia deleteRequest = new ArchiveController.DeleteMedia( IntStream @@ -537,9 +537,9 @@ public void delete() throws VerificationFailedException { @Test public void mediaUploadForm() throws RateLimitExceededException, VerificationFailedException { final BackupAuthCredentialPresentation presentation = - backupAuthTestUtil.getPresentation(BackupTier.MEDIA, backupKey, aci); + backupAuthTestUtil.getPresentation(BackupLevel.MEDIA, backupKey, aci); when(backupManager.authenticateBackupUser(any(), any())) - .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupTier.MEDIA))); + .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupLevel.MEDIA))); when(backupManager.createTemporaryAttachmentUploadDescriptor(any())) .thenReturn(new BackupUploadDescriptor(3, "abc", Map.of("k", "v"), "example.org")); final ArchiveController.UploadDescriptorResponse desc = resources.getJerseyTest() @@ -568,9 +568,9 @@ public void mediaUploadForm() throws RateLimitExceededException, VerificationFai @Test public void readAuth() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = - backupAuthTestUtil.getPresentation(BackupTier.MEDIA, backupKey, aci); + backupAuthTestUtil.getPresentation(BackupLevel.MEDIA, backupKey, aci); when(backupManager.authenticateBackupUser(any(), any())) - .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupTier.MEDIA))); + .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupLevel.MEDIA))); when(backupManager.generateReadAuth(any(), eq(3))).thenReturn(Map.of("key", "value")); final ArchiveController.ReadAuthResponse response = resources.getJerseyTest() .target("v1/archives/auth/read") @@ -585,7 +585,7 @@ public void readAuth() throws VerificationFailedException { @Test public void readAuthInvalidParam() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = - backupAuthTestUtil.getPresentation(BackupTier.MEDIA, backupKey, aci); + backupAuthTestUtil.getPresentation(BackupLevel.MEDIA, backupKey, aci); Response response = resources.getJerseyTest() .target("v1/archives/auth/read") .request() @@ -607,9 +607,9 @@ public void readAuthInvalidParam() throws VerificationFailedException { @Test public void deleteEntireBackup() throws VerificationFailedException { final BackupAuthCredentialPresentation presentation = - backupAuthTestUtil.getPresentation(BackupTier.MEDIA, backupKey, aci); + backupAuthTestUtil.getPresentation(BackupLevel.MEDIA, backupKey, aci); when(backupManager.authenticateBackupUser(any(), any())) - .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupTier.MEDIA))); + .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupLevel.MEDIA))); when(backupManager.deleteEntireBackup(any())).thenReturn(CompletableFuture.completedFuture(null)); Response response = resources.getJerseyTest() .target("v1/archives/") @@ -620,7 +620,7 @@ public void deleteEntireBackup() throws VerificationFailedException { assertThat(response.getStatus()).isEqualTo(204); } - private static AuthenticatedBackupUser backupUser(byte[] backupId, BackupTier backupTier) { - return new AuthenticatedBackupUser(backupId, backupTier, "myBackupDir", "myMediaDir"); + private static AuthenticatedBackupUser backupUser(byte[] backupId, BackupLevel backupLevel) { + return new AuthenticatedBackupUser(backupId, backupLevel, "myBackupDir", "myMediaDir"); } }