Add new upload-for-copy backup endpoint
This commit is contained in:
		
							parent
							
								
									e5d654f0c7
								
							
						
					
					
						commit
						d36df3eaa9
					
				| 
						 | 
					@ -709,6 +709,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
 | 
				
			||||||
    ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
 | 
					    ServerZkAuthOperations zkAuthOperations = new ServerZkAuthOperations(zkSecretParams);
 | 
				
			||||||
    ServerZkReceiptOperations zkReceiptOperations = new ServerZkReceiptOperations(zkSecretParams);
 | 
					    ServerZkReceiptOperations zkReceiptOperations = new ServerZkReceiptOperations(zkSecretParams);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    TusAttachmentGenerator tusAttachmentGenerator = new TusAttachmentGenerator(config.getTus());
 | 
				
			||||||
    Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator = new Cdn3BackupCredentialGenerator(config.getTus());
 | 
					    Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator = new Cdn3BackupCredentialGenerator(config.getTus());
 | 
				
			||||||
    BackupAuthManager backupAuthManager = new BackupAuthManager(experimentEnrollmentManager, rateLimiters,
 | 
					    BackupAuthManager backupAuthManager = new BackupAuthManager(experimentEnrollmentManager, rateLimiters,
 | 
				
			||||||
        accountsManager, zkReceiptOperations, redeemedReceiptsManager, backupsGenericZkSecretParams, clock);
 | 
					        accountsManager, zkReceiptOperations, redeemedReceiptsManager, backupsGenericZkSecretParams, clock);
 | 
				
			||||||
| 
						 | 
					@ -719,6 +720,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
 | 
				
			||||||
    BackupManager backupManager = new BackupManager(
 | 
					    BackupManager backupManager = new BackupManager(
 | 
				
			||||||
        backupsDb,
 | 
					        backupsDb,
 | 
				
			||||||
        backupsGenericZkSecretParams,
 | 
					        backupsGenericZkSecretParams,
 | 
				
			||||||
 | 
					        rateLimiters,
 | 
				
			||||||
 | 
					        tusAttachmentGenerator,
 | 
				
			||||||
        cdn3BackupCredentialGenerator,
 | 
					        cdn3BackupCredentialGenerator,
 | 
				
			||||||
        new Cdn3RemoteStorageManager(
 | 
					        new Cdn3RemoteStorageManager(
 | 
				
			||||||
            remoteStorageExecutor,
 | 
					            remoteStorageExecutor,
 | 
				
			||||||
| 
						 | 
					@ -947,7 +950,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
 | 
				
			||||||
            config.getAwsAttachmentsConfiguration().accessSecret().value(),
 | 
					            config.getAwsAttachmentsConfiguration().accessSecret().value(),
 | 
				
			||||||
            config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
 | 
					            config.getAwsAttachmentsConfiguration().region(), config.getAwsAttachmentsConfiguration().bucket()),
 | 
				
			||||||
        new AttachmentControllerV3(rateLimiters, gcsAttachmentGenerator),
 | 
					        new AttachmentControllerV3(rateLimiters, gcsAttachmentGenerator),
 | 
				
			||||||
        new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, new TusAttachmentGenerator(config.getTus()),
 | 
					        new AttachmentControllerV4(rateLimiters, gcsAttachmentGenerator, tusAttachmentGenerator,
 | 
				
			||||||
            experimentEnrollmentManager),
 | 
					            experimentEnrollmentManager),
 | 
				
			||||||
        new ArchiveController(backupAuthManager, backupManager),
 | 
					        new ArchiveController(backupAuthManager, backupManager),
 | 
				
			||||||
        new CallRoutingController(rateLimiters, callRouter, turnTokenGenerator),
 | 
					        new CallRoutingController(rateLimiters, callRouter, turnTokenGenerator),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@ import io.micrometer.core.instrument.DistributionSummary;
 | 
				
			||||||
import io.micrometer.core.instrument.Metrics;
 | 
					import io.micrometer.core.instrument.Metrics;
 | 
				
			||||||
import java.io.IOException;
 | 
					import java.io.IOException;
 | 
				
			||||||
import java.net.URI;
 | 
					import java.net.URI;
 | 
				
			||||||
 | 
					import java.security.SecureRandom;
 | 
				
			||||||
import java.time.Clock;
 | 
					import java.time.Clock;
 | 
				
			||||||
import java.time.Duration;
 | 
					import java.time.Duration;
 | 
				
			||||||
import java.time.Instant;
 | 
					import java.time.Instant;
 | 
				
			||||||
| 
						 | 
					@ -30,7 +31,12 @@ import org.signal.libsignal.zkgroup.VerificationFailedException;
 | 
				
			||||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
 | 
					import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
 | 
				
			||||||
import org.slf4j.Logger;
 | 
					import org.slf4j.Logger;
 | 
				
			||||||
import org.slf4j.LoggerFactory;
 | 
					import org.slf4j.LoggerFactory;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
 | 
					import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.limits.RateLimiter;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.limits.RateLimiters;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
 | 
					import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
 | 
					import org.whispersystems.textsecuregcm.util.ExceptionUtils;
 | 
				
			||||||
import reactor.core.publisher.Flux;
 | 
					import reactor.core.publisher.Flux;
 | 
				
			||||||
| 
						 | 
					@ -59,21 +65,28 @@ public class BackupManager {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private final BackupsDb backupsDb;
 | 
					  private final BackupsDb backupsDb;
 | 
				
			||||||
  private final GenericServerSecretParams serverSecretParams;
 | 
					  private final GenericServerSecretParams serverSecretParams;
 | 
				
			||||||
 | 
					  private final RateLimiters rateLimiters;
 | 
				
			||||||
 | 
					  private final TusAttachmentGenerator tusAttachmentGenerator;
 | 
				
			||||||
  private final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator;
 | 
					  private final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator;
 | 
				
			||||||
  private final RemoteStorageManager remoteStorageManager;
 | 
					  private final RemoteStorageManager remoteStorageManager;
 | 
				
			||||||
  private final Map<Integer, String> attachmentCdnBaseUris;
 | 
					  private final Map<Integer, String> attachmentCdnBaseUris;
 | 
				
			||||||
 | 
					  private final SecureRandom secureRandom = new SecureRandom();
 | 
				
			||||||
  private final Clock clock;
 | 
					  private final Clock clock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public BackupManager(
 | 
					  public BackupManager(
 | 
				
			||||||
      final BackupsDb backupsDb,
 | 
					      final BackupsDb backupsDb,
 | 
				
			||||||
      final GenericServerSecretParams serverSecretParams,
 | 
					      final GenericServerSecretParams serverSecretParams,
 | 
				
			||||||
 | 
					      final RateLimiters rateLimiters,
 | 
				
			||||||
 | 
					      final TusAttachmentGenerator tusAttachmentGenerator,
 | 
				
			||||||
      final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator,
 | 
					      final Cdn3BackupCredentialGenerator cdn3BackupCredentialGenerator,
 | 
				
			||||||
      final RemoteStorageManager remoteStorageManager,
 | 
					      final RemoteStorageManager remoteStorageManager,
 | 
				
			||||||
      final Map<Integer, String> attachmentCdnBaseUris,
 | 
					      final Map<Integer, String> attachmentCdnBaseUris,
 | 
				
			||||||
      final Clock clock) {
 | 
					      final Clock clock) {
 | 
				
			||||||
    this.backupsDb = backupsDb;
 | 
					    this.backupsDb = backupsDb;
 | 
				
			||||||
    this.serverSecretParams = serverSecretParams;
 | 
					    this.serverSecretParams = serverSecretParams;
 | 
				
			||||||
 | 
					    this.rateLimiters = rateLimiters;
 | 
				
			||||||
 | 
					    this.tusAttachmentGenerator = tusAttachmentGenerator;
 | 
				
			||||||
    this.cdn3BackupCredentialGenerator = cdn3BackupCredentialGenerator;
 | 
					    this.cdn3BackupCredentialGenerator = cdn3BackupCredentialGenerator;
 | 
				
			||||||
    this.remoteStorageManager = remoteStorageManager;
 | 
					    this.remoteStorageManager = remoteStorageManager;
 | 
				
			||||||
    this.clock = clock;
 | 
					    this.clock = clock;
 | 
				
			||||||
| 
						 | 
					@ -131,26 +144,38 @@ public class BackupManager {
 | 
				
			||||||
   * @param backupUser an already ZK authenticated backup user
 | 
					   * @param backupUser an already ZK authenticated backup user
 | 
				
			||||||
   * @return the upload form
 | 
					   * @return the upload form
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public CompletableFuture<MessageBackupUploadDescriptor> createMessageBackupUploadDescriptor(
 | 
					  public CompletableFuture<BackupUploadDescriptor> createMessageBackupUploadDescriptor(
 | 
				
			||||||
      final AuthenticatedBackupUser backupUser) {
 | 
					      final AuthenticatedBackupUser backupUser) {
 | 
				
			||||||
 | 
					    checkBackupTier(backupUser, BackupTier.MESSAGES);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
 | 
					    // this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
 | 
				
			||||||
    return backupsDb
 | 
					    return backupsDb
 | 
				
			||||||
        .addMessageBackup(backupUser)
 | 
					        .addMessageBackup(backupUser)
 | 
				
			||||||
        .thenApply(result -> cdn3BackupCredentialGenerator.generateUpload(cdnMessageBackupName(backupUser)));
 | 
					        .thenApply(result -> cdn3BackupCredentialGenerator.generateUpload(cdnMessageBackupName(backupUser)));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public BackupUploadDescriptor createTemporaryAttachmentUploadDescriptor(final AuthenticatedBackupUser backupUser)
 | 
				
			||||||
 | 
					      throws RateLimitExceededException {
 | 
				
			||||||
 | 
					    checkBackupTier(backupUser, BackupTier.MEDIA);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    RateLimiter.adaptLegacyException(() -> rateLimiters
 | 
				
			||||||
 | 
					        .forDescriptor(RateLimiters.For.BACKUP_ATTACHMENT)
 | 
				
			||||||
 | 
					        .validate(rateLimitKey(backupUser)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final byte[] bytes = new byte[15];
 | 
				
			||||||
 | 
					    secureRandom.nextBytes(bytes);
 | 
				
			||||||
 | 
					    final String attachmentKey = Base64.getUrlEncoder().encodeToString(bytes);
 | 
				
			||||||
 | 
					    final AttachmentGenerator.Descriptor descriptor = tusAttachmentGenerator.generateAttachment(attachmentKey);
 | 
				
			||||||
 | 
					    return new BackupUploadDescriptor(3, attachmentKey, descriptor.headers(), descriptor.signedUploadLocation());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Update the last update timestamps for the backupId in the presentation
 | 
					   * Update the last update timestamps for the backupId in the presentation
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
   * @param backupUser an already ZK authenticated backup user
 | 
					   * @param backupUser an already ZK authenticated backup user
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
 | 
					  public CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
 | 
				
			||||||
    if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
 | 
					    checkBackupTier(backupUser, BackupTier.MESSAGES);
 | 
				
			||||||
      Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
 | 
					 | 
				
			||||||
      throw Status.PERMISSION_DENIED
 | 
					 | 
				
			||||||
          .withDescription("credential does not support ttl operation")
 | 
					 | 
				
			||||||
          .asRuntimeException();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // update message backup TTL
 | 
					    // update message backup TTL
 | 
				
			||||||
    return backupsDb.ttlRefresh(backupUser);
 | 
					    return backupsDb.ttlRefresh(backupUser);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -165,11 +190,7 @@ public class BackupManager {
 | 
				
			||||||
   * @return Information about the existing backup
 | 
					   * @return Information about the existing backup
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public CompletableFuture<BackupInfo> backupInfo(final AuthenticatedBackupUser backupUser) {
 | 
					  public CompletableFuture<BackupInfo> backupInfo(final AuthenticatedBackupUser backupUser) {
 | 
				
			||||||
    if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
 | 
					    checkBackupTier(backupUser, BackupTier.MESSAGES);
 | 
				
			||||||
      Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
 | 
					 | 
				
			||||||
      throw Status.PERMISSION_DENIED.withDescription("credential does not support info operation")
 | 
					 | 
				
			||||||
          .asRuntimeException();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return backupsDb.describeBackup(backupUser)
 | 
					    return backupsDb.describeBackup(backupUser)
 | 
				
			||||||
        .thenApply(backupDescription -> new BackupInfo(
 | 
					        .thenApply(backupDescription -> new BackupInfo(
 | 
				
			||||||
            backupDescription.cdn(),
 | 
					            backupDescription.cdn(),
 | 
				
			||||||
| 
						 | 
					@ -187,12 +208,7 @@ public class BackupManager {
 | 
				
			||||||
   * @return true if mediaLength bytes can be stored
 | 
					   * @return true if mediaLength bytes can be stored
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public CompletableFuture<Boolean> canStoreMedia(final AuthenticatedBackupUser backupUser, final long mediaLength) {
 | 
					  public CompletableFuture<Boolean> canStoreMedia(final AuthenticatedBackupUser backupUser, final long mediaLength) {
 | 
				
			||||||
    if (backupUser.backupTier().compareTo(BackupTier.MEDIA) < 0) {
 | 
					    checkBackupTier(backupUser, BackupTier.MEDIA);
 | 
				
			||||||
      Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
 | 
					 | 
				
			||||||
      throw Status.PERMISSION_DENIED
 | 
					 | 
				
			||||||
          .withDescription("credential does not support storing media")
 | 
					 | 
				
			||||||
          .asRuntimeException();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return backupsDb.getMediaUsage(backupUser)
 | 
					    return backupsDb.getMediaUsage(backupUser)
 | 
				
			||||||
        .thenComposeAsync(info -> {
 | 
					        .thenComposeAsync(info -> {
 | 
				
			||||||
          final boolean canStore = MAX_TOTAL_BACKUP_MEDIA_BYTES - info.usageInfo().bytesUsed() >= mediaLength;
 | 
					          final boolean canStore = MAX_TOTAL_BACKUP_MEDIA_BYTES - info.usageInfo().bytesUsed() >= mediaLength;
 | 
				
			||||||
| 
						 | 
					@ -243,12 +259,7 @@ public class BackupManager {
 | 
				
			||||||
      final int sourceLength,
 | 
					      final int sourceLength,
 | 
				
			||||||
      final MediaEncryptionParameters encryptionParameters,
 | 
					      final MediaEncryptionParameters encryptionParameters,
 | 
				
			||||||
      final byte[] destinationMediaId) {
 | 
					      final byte[] destinationMediaId) {
 | 
				
			||||||
    if (backupUser.backupTier().compareTo(BackupTier.MEDIA) < 0) {
 | 
					    checkBackupTier(backupUser, BackupTier.MEDIA);
 | 
				
			||||||
      Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
 | 
					 | 
				
			||||||
      throw Status.PERMISSION_DENIED
 | 
					 | 
				
			||||||
          .withDescription("credential does not support storing media")
 | 
					 | 
				
			||||||
          .asRuntimeException();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (sourceLength > MAX_MEDIA_OBJECT_SIZE) {
 | 
					    if (sourceLength > MAX_MEDIA_OBJECT_SIZE) {
 | 
				
			||||||
      throw Status.INVALID_ARGUMENT
 | 
					      throw Status.INVALID_ARGUMENT
 | 
				
			||||||
          .withDescription("Invalid sourceObject size")
 | 
					          .withDescription("Invalid sourceObject size")
 | 
				
			||||||
| 
						 | 
					@ -262,7 +273,7 @@ public class BackupManager {
 | 
				
			||||||
      return CompletableFuture.failedFuture(e);
 | 
					      return CompletableFuture.failedFuture(e);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final MessageBackupUploadDescriptor dst = cdn3BackupCredentialGenerator.generateUpload(
 | 
					    final BackupUploadDescriptor dst = cdn3BackupCredentialGenerator.generateUpload(
 | 
				
			||||||
        cdnMediaPath(backupUser, destinationMediaId));
 | 
					        cdnMediaPath(backupUser, destinationMediaId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final int destinationLength = encryptionParameters.outputSize(sourceLength);
 | 
					    final int destinationLength = encryptionParameters.outputSize(sourceLength);
 | 
				
			||||||
| 
						 | 
					@ -309,12 +320,7 @@ public class BackupManager {
 | 
				
			||||||
   * @return A map of headers to include with CDN requests
 | 
					   * @return A map of headers to include with CDN requests
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backupUser) {
 | 
					  public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backupUser) {
 | 
				
			||||||
    if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
 | 
					    checkBackupTier(backupUser, BackupTier.MESSAGES);
 | 
				
			||||||
      Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
 | 
					 | 
				
			||||||
      throw Status.PERMISSION_DENIED
 | 
					 | 
				
			||||||
          .withDescription("credential does not support read auth operation")
 | 
					 | 
				
			||||||
          .asRuntimeException();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return cdn3BackupCredentialGenerator.readHeaders(backupUser.backupDir());
 | 
					    return cdn3BackupCredentialGenerator.readHeaders(backupUser.backupDir());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -339,12 +345,7 @@ public class BackupManager {
 | 
				
			||||||
      final AuthenticatedBackupUser backupUser,
 | 
					      final AuthenticatedBackupUser backupUser,
 | 
				
			||||||
      final Optional<String> cursor,
 | 
					      final Optional<String> cursor,
 | 
				
			||||||
      final int limit) {
 | 
					      final int limit) {
 | 
				
			||||||
    if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
 | 
					    checkBackupTier(backupUser, BackupTier.MESSAGES);
 | 
				
			||||||
      Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
 | 
					 | 
				
			||||||
      throw Status.PERMISSION_DENIED
 | 
					 | 
				
			||||||
          .withDescription("credential does not support list operation")
 | 
					 | 
				
			||||||
          .asRuntimeException();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return remoteStorageManager.list(cdnMediaDirectory(backupUser), cursor, limit)
 | 
					    return remoteStorageManager.list(cdnMediaDirectory(backupUser), cursor, limit)
 | 
				
			||||||
        .thenApply(result ->
 | 
					        .thenApply(result ->
 | 
				
			||||||
            new ListMediaResult(
 | 
					            new ListMediaResult(
 | 
				
			||||||
| 
						 | 
					@ -370,12 +371,7 @@ public class BackupManager {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public CompletableFuture<Void> delete(final AuthenticatedBackupUser backupUser,
 | 
					  public CompletableFuture<Void> delete(final AuthenticatedBackupUser backupUser,
 | 
				
			||||||
      final List<StorageDescriptor> storageDescriptors) {
 | 
					      final List<StorageDescriptor> storageDescriptors) {
 | 
				
			||||||
    if (backupUser.backupTier().compareTo(BackupTier.MESSAGES) < 0) {
 | 
					    checkBackupTier(backupUser, BackupTier.MESSAGES);
 | 
				
			||||||
      Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
 | 
					 | 
				
			||||||
      throw Status.PERMISSION_DENIED
 | 
					 | 
				
			||||||
          .withDescription("credential does not support list operation")
 | 
					 | 
				
			||||||
          .asRuntimeException();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (storageDescriptors.stream().anyMatch(sd -> sd.cdn() != remoteStorageManager.cdnNumber())) {
 | 
					    if (storageDescriptors.stream().anyMatch(sd -> sd.cdn() != remoteStorageManager.cdnNumber())) {
 | 
				
			||||||
      throw Status.INVALID_ARGUMENT
 | 
					      throw Status.INVALID_ARGUMENT
 | 
				
			||||||
| 
						 | 
					@ -430,6 +426,7 @@ public class BackupManager {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static final ECPublicKey INVALID_PUBLIC_KEY = Curve.generateKeyPair().getPublicKey();
 | 
					  private static final ECPublicKey INVALID_PUBLIC_KEY = Curve.generateKeyPair().getPublicKey();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Authenticate the ZK anonymous backup credential's presentation
 | 
					   * Authenticate the ZK anonymous backup credential's presentation
 | 
				
			||||||
   * <p>
 | 
					   * <p>
 | 
				
			||||||
| 
						 | 
					@ -532,6 +529,7 @@ public class BackupManager {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface PresentationSignatureVerifier {
 | 
					  interface PresentationSignatureVerifier {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    BackupTier verifySignature(byte[] signature, ECPublicKey publicKey);
 | 
					    BackupTier verifySignature(byte[] signature, ECPublicKey publicKey);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -576,6 +574,22 @@ public class BackupManager {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Check that the authenticated backup user is authorized to use the provided backupTier
 | 
				
			||||||
 | 
					   *
 | 
				
			||||||
 | 
					   * @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}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  private static void checkBackupTier(final AuthenticatedBackupUser backupUser, final BackupTier backupTier) {
 | 
				
			||||||
 | 
					    if (backupUser.backupTier().compareTo(backupTier) < 0) {
 | 
				
			||||||
 | 
					      Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
 | 
				
			||||||
 | 
					      throw Status.PERMISSION_DENIED
 | 
				
			||||||
 | 
					          .withDescription("credential does not support the requested operation")
 | 
				
			||||||
 | 
					          .asRuntimeException();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @VisibleForTesting
 | 
					  @VisibleForTesting
 | 
				
			||||||
  static String encodeMediaIdForCdn(final byte[] bytes) {
 | 
					  static String encodeMediaIdForCdn(final byte[] bytes) {
 | 
				
			||||||
    return Base64.getUrlEncoder().encodeToString(bytes);
 | 
					    return Base64.getUrlEncoder().encodeToString(bytes);
 | 
				
			||||||
| 
						 | 
					@ -596,4 +610,8 @@ public class BackupManager {
 | 
				
			||||||
  private static String cdnMediaPath(final AuthenticatedBackupUser backupUser, final byte[] mediaId) {
 | 
					  private static String cdnMediaPath(final AuthenticatedBackupUser backupUser, final byte[] mediaId) {
 | 
				
			||||||
    return "%s%s".formatted(cdnMediaDirectory(backupUser), encodeMediaIdForCdn(mediaId));
 | 
					    return "%s%s".formatted(cdnMediaDirectory(backupUser), encodeMediaIdForCdn(mediaId));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static String rateLimitKey(final AuthenticatedBackupUser backupUser) {
 | 
				
			||||||
 | 
					    return Base64.getEncoder().encodeToString(BackupsDb.hashedBackupId(backupUser.backupId()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ package org.whispersystems.textsecuregcm.backup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public record MessageBackupUploadDescriptor(
 | 
					public record BackupUploadDescriptor(
 | 
				
			||||||
    int cdn,
 | 
					    int cdn,
 | 
				
			||||||
    String key,
 | 
					    String key,
 | 
				
			||||||
    Map<String, String> headers,
 | 
					    Map<String, String> headers,
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,7 @@ public class Cdn3BackupCredentialGenerator {
 | 
				
			||||||
        .build();
 | 
					        .build();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public MessageBackupUploadDescriptor generateUpload(final String key) {
 | 
					  public BackupUploadDescriptor generateUpload(final String key) {
 | 
				
			||||||
    if (key.isBlank()) {
 | 
					    if (key.isBlank()) {
 | 
				
			||||||
      throw new IllegalArgumentException("Upload descriptors must have non-empty keys");
 | 
					      throw new IllegalArgumentException("Upload descriptors must have non-empty keys");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -60,7 +60,7 @@ public class Cdn3BackupCredentialGenerator {
 | 
				
			||||||
        HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials),
 | 
					        HttpHeaders.AUTHORIZATION, HeaderUtils.basicAuthHeader(credentials),
 | 
				
			||||||
        "Upload-Metadata", String.format("filename %s", b64Key));
 | 
					        "Upload-Metadata", String.format("filename %s", b64Key));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return new MessageBackupUploadDescriptor(
 | 
					    return new BackupUploadDescriptor(
 | 
				
			||||||
        BACKUP_CDN,
 | 
					        BACKUP_CDN,
 | 
				
			||||||
        key,
 | 
					        key,
 | 
				
			||||||
        headers,
 | 
					        headers,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -113,7 +113,7 @@ public class Cdn3RemoteStorageManager implements RemoteStorageManager {
 | 
				
			||||||
      final URI sourceUri,
 | 
					      final URI sourceUri,
 | 
				
			||||||
      final int expectedSourceLength,
 | 
					      final int expectedSourceLength,
 | 
				
			||||||
      final MediaEncryptionParameters encryptionParameters,
 | 
					      final MediaEncryptionParameters encryptionParameters,
 | 
				
			||||||
      final MessageBackupUploadDescriptor uploadDescriptor) {
 | 
					      final BackupUploadDescriptor uploadDescriptor) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (uploadDescriptor.cdn() != cdnNumber()) {
 | 
					    if (uploadDescriptor.cdn() != cdnNumber()) {
 | 
				
			||||||
      throw new IllegalArgumentException("Cdn3RemoteStorageManager can only copy to cdn3");
 | 
					      throw new IllegalArgumentException("Cdn3RemoteStorageManager can only copy to cdn3");
 | 
				
			||||||
| 
						 | 
					@ -152,7 +152,7 @@ public class Cdn3RemoteStorageManager implements RemoteStorageManager {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private HttpRequest createCopyRequest(
 | 
					  private HttpRequest createCopyRequest(
 | 
				
			||||||
      final int expectedSourceLength,
 | 
					      final int expectedSourceLength,
 | 
				
			||||||
      final MessageBackupUploadDescriptor uploadDescriptor,
 | 
					      final BackupUploadDescriptor uploadDescriptor,
 | 
				
			||||||
      BackupMediaEncrypter encrypter,
 | 
					      BackupMediaEncrypter encrypter,
 | 
				
			||||||
      HttpResponse<Flow.Publisher<List<ByteBuffer>>> response) throws IOException {
 | 
					      HttpResponse<Flow.Publisher<List<ByteBuffer>>> response) throws IOException {
 | 
				
			||||||
    if (response.statusCode() == Response.Status.NOT_FOUND.getStatusCode()) {
 | 
					    if (response.statusCode() == Response.Status.NOT_FOUND.getStatusCode()) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,7 @@ public interface RemoteStorageManager {
 | 
				
			||||||
      URI sourceUri,
 | 
					      URI sourceUri,
 | 
				
			||||||
      int expectedSourceLength,
 | 
					      int expectedSourceLength,
 | 
				
			||||||
      MediaEncryptionParameters encryptionParameters,
 | 
					      MediaEncryptionParameters encryptionParameters,
 | 
				
			||||||
      MessageBackupUploadDescriptor uploadDescriptor);
 | 
					      BackupUploadDescriptor uploadDescriptor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Result of a {@link #list} operation
 | 
					   * Result of a {@link #list} operation
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -384,7 +384,7 @@ public class ArchiveController {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public record MessageBackupResponse(
 | 
					  public record UploadDescriptorResponse(
 | 
				
			||||||
      @Schema(description = "Indicates the CDN type. 3 indicates resumable uploads using TUS")
 | 
					      @Schema(description = "Indicates the CDN type. 3 indicates resumable uploads using TUS")
 | 
				
			||||||
      int cdn,
 | 
					      int cdn,
 | 
				
			||||||
      @Schema(description = "The location within the specified cdn where the finished upload can be found.")
 | 
					      @Schema(description = "The location within the specified cdn where the finished upload can be found.")
 | 
				
			||||||
| 
						 | 
					@ -400,10 +400,10 @@ public class ArchiveController {
 | 
				
			||||||
  @Operation(
 | 
					  @Operation(
 | 
				
			||||||
      summary = "Fetch message backup upload form",
 | 
					      summary = "Fetch message backup upload form",
 | 
				
			||||||
      description = "Retrieve an upload form that can be used to perform a resumable upload of a message backup.")
 | 
					      description = "Retrieve an upload form that can be used to perform a resumable upload of a message backup.")
 | 
				
			||||||
  @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = MessageBackupResponse.class)))
 | 
					  @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = UploadDescriptorResponse.class)))
 | 
				
			||||||
  @ApiResponse(responseCode = "429", description = "Rate limited.")
 | 
					  @ApiResponse(responseCode = "429", description = "Rate limited.")
 | 
				
			||||||
  @ApiResponseZkAuth
 | 
					  @ApiResponseZkAuth
 | 
				
			||||||
  public CompletionStage<MessageBackupResponse> backup(
 | 
					  public CompletionStage<UploadDescriptorResponse> backup(
 | 
				
			||||||
      @ReadOnly @Auth final Optional<AuthenticatedAccount> account,
 | 
					      @ReadOnly @Auth final Optional<AuthenticatedAccount> account,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
 | 
					      @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
 | 
				
			||||||
| 
						 | 
					@ -418,7 +418,49 @@ public class ArchiveController {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return backupManager.authenticateBackupUser(presentation.presentation, signature.signature)
 | 
					    return backupManager.authenticateBackupUser(presentation.presentation, signature.signature)
 | 
				
			||||||
        .thenCompose(backupManager::createMessageBackupUploadDescriptor)
 | 
					        .thenCompose(backupManager::createMessageBackupUploadDescriptor)
 | 
				
			||||||
        .thenApply(result -> new MessageBackupResponse(
 | 
					        .thenApply(result -> new UploadDescriptorResponse(
 | 
				
			||||||
 | 
					            result.cdn(),
 | 
				
			||||||
 | 
					            result.key(),
 | 
				
			||||||
 | 
					            result.headers(),
 | 
				
			||||||
 | 
					            result.signedUploadLocation()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @GET
 | 
				
			||||||
 | 
					  @Path("/media/upload/form")
 | 
				
			||||||
 | 
					  @Produces(MediaType.APPLICATION_JSON)
 | 
				
			||||||
 | 
					  @Operation(
 | 
				
			||||||
 | 
					      summary = "Fetch media attachment upload form",
 | 
				
			||||||
 | 
					      description = """
 | 
				
			||||||
 | 
					          Retrieve an upload form that can be used to perform a resumable upload of an attachment. After uploading, the
 | 
				
			||||||
 | 
					          attachment can be copied into the backup at PUT /archives/media/.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          Like the account authenticated version at /attachments, the uploaded object is only temporary.
 | 
				
			||||||
 | 
					          """)
 | 
				
			||||||
 | 
					  @ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = UploadDescriptorResponse.class)))
 | 
				
			||||||
 | 
					  @ApiResponse(responseCode = "429", description = "Rate limited.")
 | 
				
			||||||
 | 
					  @ApiResponseZkAuth
 | 
				
			||||||
 | 
					  public CompletionStage<UploadDescriptorResponse> uploadTemporaryAttachment(
 | 
				
			||||||
 | 
					      @ReadOnly @Auth final Optional<AuthenticatedAccount> account,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @Parameter(description = BackupAuthCredentialPresentationHeader.DESCRIPTION, schema = @Schema(implementation = String.class))
 | 
				
			||||||
 | 
					      @NotNull
 | 
				
			||||||
 | 
					      @HeaderParam(X_SIGNAL_ZK_AUTH) final ArchiveController.BackupAuthCredentialPresentationHeader presentation,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @Parameter(description = BackupAuthCredentialPresentationSignature.DESCRIPTION, schema = @Schema(implementation = String.class))
 | 
				
			||||||
 | 
					      @NotNull
 | 
				
			||||||
 | 
					      @HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature) {
 | 
				
			||||||
 | 
					    if (account.isPresent()) {
 | 
				
			||||||
 | 
					      throw new BadRequestException("must not use authenticated connection for anonymous operations");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return backupManager.authenticateBackupUser(presentation.presentation, signature.signature)
 | 
				
			||||||
 | 
					        .thenApply(backupUser -> {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            return backupManager.createTemporaryAttachmentUploadDescriptor(backupUser);
 | 
				
			||||||
 | 
					          } catch (RateLimitExceededException e) {
 | 
				
			||||||
 | 
					            throw ExceptionUtils.wrap(e);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .thenApply(result -> new UploadDescriptorResponse(
 | 
				
			||||||
            result.cdn(),
 | 
					            result.cdn(),
 | 
				
			||||||
            result.key(),
 | 
					            result.key(),
 | 
				
			||||||
            result.headers(),
 | 
					            result.headers(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,7 @@ public class RateLimiters extends BaseRateLimiters<RateLimiters.For> {
 | 
				
			||||||
    VERIFY("verify", false, new RateLimiterConfig(6, Duration.ofSeconds(30))),
 | 
					    VERIFY("verify", false, new RateLimiterConfig(6, Duration.ofSeconds(30))),
 | 
				
			||||||
    PIN("pin", false, new RateLimiterConfig(10, Duration.ofDays(1))),
 | 
					    PIN("pin", false, new RateLimiterConfig(10, Duration.ofDays(1))),
 | 
				
			||||||
    ATTACHMENT("attachmentCreate", false, new RateLimiterConfig(50, Duration.ofMillis(1200))),
 | 
					    ATTACHMENT("attachmentCreate", false, new RateLimiterConfig(50, Duration.ofMillis(1200))),
 | 
				
			||||||
 | 
					    BACKUP_ATTACHMENT("backupAttachmentCreate", true, new RateLimiterConfig(10_000, Duration.ofSeconds(1))),
 | 
				
			||||||
    PRE_KEYS("prekeys", false, new RateLimiterConfig(6, Duration.ofMinutes(10))),
 | 
					    PRE_KEYS("prekeys", false, new RateLimiterConfig(6, Duration.ofMinutes(10))),
 | 
				
			||||||
    MESSAGES("messages", false, new RateLimiterConfig(60, Duration.ofSeconds(1))),
 | 
					    MESSAGES("messages", false, new RateLimiterConfig(60, Duration.ofSeconds(1))),
 | 
				
			||||||
    STORIES("stories", false, new RateLimiterConfig(5_000, Duration.ofSeconds(8))),
 | 
					    STORIES("stories", false, new RateLimiterConfig(5_000, Duration.ofSeconds(8))),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ import org.signal.libsignal.zkgroup.GenericServerSecretParams;
 | 
				
			||||||
import org.signal.libsignal.zkgroup.InvalidInputException;
 | 
					import org.signal.libsignal.zkgroup.InvalidInputException;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
 | 
					import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.WhisperServerService;
 | 
					import org.whispersystems.textsecuregcm.WhisperServerService;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
 | 
					import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.backup.BackupManager;
 | 
					import org.whispersystems.textsecuregcm.backup.BackupManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.backup.BackupsDb;
 | 
					import org.whispersystems.textsecuregcm.backup.BackupsDb;
 | 
				
			||||||
| 
						 | 
					@ -27,6 +28,7 @@ import org.whispersystems.textsecuregcm.backup.Cdn3RemoteStorageManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
 | 
					import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
 | 
					import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
 | 
					import org.whispersystems.textsecuregcm.controllers.SecureValueRecovery2Controller;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.limits.RateLimiters;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
 | 
					import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
 | 
					import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.redis.ClusterFaultTolerantRedisCluster;
 | 
					import org.whispersystems.textsecuregcm.redis.ClusterFaultTolerantRedisCluster;
 | 
				
			||||||
| 
						 | 
					@ -198,6 +200,8 @@ record CommandDependencies(
 | 
				
			||||||
        secureStorageClient, secureValueRecovery2Client, clientPresenceManager,
 | 
					        secureStorageClient, secureValueRecovery2Client, clientPresenceManager,
 | 
				
			||||||
        registrationRecoveryPasswordsManager, accountLockExecutor, clientPresenceExecutor,
 | 
					        registrationRecoveryPasswordsManager, accountLockExecutor, clientPresenceExecutor,
 | 
				
			||||||
        clock);
 | 
					        clock);
 | 
				
			||||||
 | 
					    RateLimiters rateLimiters = RateLimiters.createAndValidate(configuration.getLimitsConfiguration(),
 | 
				
			||||||
 | 
					        dynamicConfigurationManager, rateLimitersCluster);
 | 
				
			||||||
    final BackupsDb backupsDb =
 | 
					    final BackupsDb backupsDb =
 | 
				
			||||||
        new BackupsDb(dynamoDbAsyncClient, configuration.getDynamoDbTables().getBackups().getTableName(), clock);
 | 
					        new BackupsDb(dynamoDbAsyncClient, configuration.getDynamoDbTables().getBackups().getTableName(), clock);
 | 
				
			||||||
    final GenericServerSecretParams backupsGenericZkSecretParams;
 | 
					    final GenericServerSecretParams backupsGenericZkSecretParams;
 | 
				
			||||||
| 
						 | 
					@ -210,6 +214,8 @@ record CommandDependencies(
 | 
				
			||||||
    final BackupManager backupManager = new BackupManager(
 | 
					    final BackupManager backupManager = new BackupManager(
 | 
				
			||||||
        backupsDb,
 | 
					        backupsDb,
 | 
				
			||||||
        backupsGenericZkSecretParams,
 | 
					        backupsGenericZkSecretParams,
 | 
				
			||||||
 | 
					        rateLimiters,
 | 
				
			||||||
 | 
					        new TusAttachmentGenerator(configuration.getTus()),
 | 
				
			||||||
        new Cdn3BackupCredentialGenerator(configuration.getTus()),
 | 
					        new Cdn3BackupCredentialGenerator(configuration.getTus()),
 | 
				
			||||||
        new Cdn3RemoteStorageManager(
 | 
					        new Cdn3RemoteStorageManager(
 | 
				
			||||||
            remoteStorageExecutor,
 | 
					            remoteStorageExecutor,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ import static org.mockito.ArgumentMatchers.any;
 | 
				
			||||||
import static org.mockito.ArgumentMatchers.anyLong;
 | 
					import static org.mockito.ArgumentMatchers.anyLong;
 | 
				
			||||||
import static org.mockito.ArgumentMatchers.anyString;
 | 
					import static org.mockito.ArgumentMatchers.anyString;
 | 
				
			||||||
import static org.mockito.ArgumentMatchers.eq;
 | 
					import static org.mockito.ArgumentMatchers.eq;
 | 
				
			||||||
 | 
					import static org.mockito.Mockito.doThrow;
 | 
				
			||||||
import static org.mockito.Mockito.mock;
 | 
					import static org.mockito.Mockito.mock;
 | 
				
			||||||
import static org.mockito.Mockito.reset;
 | 
					import static org.mockito.Mockito.reset;
 | 
				
			||||||
import static org.mockito.Mockito.times;
 | 
					import static org.mockito.Mockito.times;
 | 
				
			||||||
| 
						 | 
					@ -60,7 +61,11 @@ import org.signal.libsignal.protocol.ecc.ECKeyPair;
 | 
				
			||||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
 | 
					import org.signal.libsignal.zkgroup.GenericServerSecretParams;
 | 
				
			||||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
 | 
					import org.signal.libsignal.zkgroup.VerificationFailedException;
 | 
				
			||||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
 | 
					import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
 | 
					import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.limits.RateLimiter;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.limits.RateLimiters;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.DynamoDbExtension;
 | 
					import org.whispersystems.textsecuregcm.storage.DynamoDbExtension;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema;
 | 
					import org.whispersystems.textsecuregcm.storage.DynamoDbExtensionSchema;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
 | 
					import org.whispersystems.textsecuregcm.util.AttributeValues;
 | 
				
			||||||
| 
						 | 
					@ -79,6 +84,8 @@ public class BackupManagerTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private final TestClock testClock = TestClock.now();
 | 
					  private final TestClock testClock = TestClock.now();
 | 
				
			||||||
  private final BackupAuthTestUtil backupAuthTestUtil = new BackupAuthTestUtil(testClock);
 | 
					  private final BackupAuthTestUtil backupAuthTestUtil = new BackupAuthTestUtil(testClock);
 | 
				
			||||||
 | 
					  private final RateLimiter mediaUploadLimiter = mock(RateLimiter.class);
 | 
				
			||||||
 | 
					  private final TusAttachmentGenerator tusAttachmentGenerator = mock(TusAttachmentGenerator.class);
 | 
				
			||||||
  private final Cdn3BackupCredentialGenerator tusCredentialGenerator = mock(Cdn3BackupCredentialGenerator.class);
 | 
					  private final Cdn3BackupCredentialGenerator tusCredentialGenerator = mock(Cdn3BackupCredentialGenerator.class);
 | 
				
			||||||
  private final RemoteStorageManager remoteStorageManager = mock(RemoteStorageManager.class);
 | 
					  private final RemoteStorageManager remoteStorageManager = mock(RemoteStorageManager.class);
 | 
				
			||||||
  private final byte[] backupKey = TestRandomUtil.nextBytes(32);
 | 
					  private final byte[] backupKey = TestRandomUtil.nextBytes(32);
 | 
				
			||||||
| 
						 | 
					@ -90,8 +97,12 @@ public class BackupManagerTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @BeforeEach
 | 
					  @BeforeEach
 | 
				
			||||||
  public void setup() {
 | 
					  public void setup() {
 | 
				
			||||||
    reset(tusCredentialGenerator);
 | 
					    reset(tusCredentialGenerator, mediaUploadLimiter);
 | 
				
			||||||
    testClock.unpin();
 | 
					    testClock.unpin();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final RateLimiters rateLimiters = mock(RateLimiters.class);
 | 
				
			||||||
 | 
					    when(rateLimiters.forDescriptor(RateLimiters.For.BACKUP_ATTACHMENT)).thenReturn(mediaUploadLimiter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.backupsDb = new BackupsDb(
 | 
					    this.backupsDb = new BackupsDb(
 | 
				
			||||||
        DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
 | 
					        DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
 | 
				
			||||||
        DynamoDbExtensionSchema.Tables.BACKUPS.tableName(),
 | 
					        DynamoDbExtensionSchema.Tables.BACKUPS.tableName(),
 | 
				
			||||||
| 
						 | 
					@ -99,6 +110,8 @@ public class BackupManagerTest {
 | 
				
			||||||
    this.backupManager = new BackupManager(
 | 
					    this.backupManager = new BackupManager(
 | 
				
			||||||
        backupsDb,
 | 
					        backupsDb,
 | 
				
			||||||
        backupAuthTestUtil.params,
 | 
					        backupAuthTestUtil.params,
 | 
				
			||||||
 | 
					        rateLimiters,
 | 
				
			||||||
 | 
					        tusAttachmentGenerator,
 | 
				
			||||||
        tusCredentialGenerator,
 | 
					        tusCredentialGenerator,
 | 
				
			||||||
        remoteStorageManager,
 | 
					        remoteStorageManager,
 | 
				
			||||||
        Map.of(3, "cdn3.example.org/attachments"),
 | 
					        Map.of(3, "cdn3.example.org/attachments"),
 | 
				
			||||||
| 
						 | 
					@ -127,6 +140,28 @@ public class BackupManagerTest {
 | 
				
			||||||
    checkExpectedExpirations(now, backupTier == BackupTier.MEDIA ? now : null, backupUser);
 | 
					    checkExpectedExpirations(now, backupTier == BackupTier.MEDIA ? now : null, backupUser);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  public void createTemporaryMediaAttachmentRateLimited() throws RateLimitExceededException {
 | 
				
			||||||
 | 
					    final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
 | 
				
			||||||
 | 
					    doThrow(new RateLimitExceededException(null, true))
 | 
				
			||||||
 | 
					        .when(mediaUploadLimiter)
 | 
				
			||||||
 | 
					        .validate(eq(BackupManager.rateLimitKey(backupUser)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assertThatExceptionOfType(RateLimitExceededException.class)
 | 
				
			||||||
 | 
					        .isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser))
 | 
				
			||||||
 | 
					        .satisfies(e -> assertThat(e.isLegacy()).isFalse());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  public void createTemporaryMediaAttachmentWrongTier() throws RateLimitExceededException {
 | 
				
			||||||
 | 
					    final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MESSAGES);
 | 
				
			||||||
 | 
					    assertThatExceptionOfType(StatusRuntimeException.class)
 | 
				
			||||||
 | 
					        .isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser))
 | 
				
			||||||
 | 
					        .extracting(StatusRuntimeException::getStatus)
 | 
				
			||||||
 | 
					        .extracting(Status::getCode)
 | 
				
			||||||
 | 
					        .isEqualTo(Status.Code.PERMISSION_DENIED);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ParameterizedTest
 | 
					  @ParameterizedTest
 | 
				
			||||||
  @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = {"NONE"})
 | 
					  @EnumSource(mode = EnumSource.Mode.EXCLUDE, names = {"NONE"})
 | 
				
			||||||
  public void ttlRefresh(final BackupTier backupTier) {
 | 
					  public void ttlRefresh(final BackupTier backupTier) {
 | 
				
			||||||
| 
						 | 
					@ -317,7 +352,7 @@ public class BackupManagerTest {
 | 
				
			||||||
  public void copySuccess() {
 | 
					  public void copySuccess() {
 | 
				
			||||||
    final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
 | 
					    final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
 | 
				
			||||||
    when(tusCredentialGenerator.generateUpload(any()))
 | 
					    when(tusCredentialGenerator.generateUpload(any()))
 | 
				
			||||||
        .thenReturn(new MessageBackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
 | 
					        .thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
 | 
				
			||||||
    when(remoteStorageManager.copy(eq(URI.create("cdn3.example.org/attachments/abc")), eq(100), any(), any()))
 | 
					    when(remoteStorageManager.copy(eq(URI.create("cdn3.example.org/attachments/abc")), eq(100), any(), any()))
 | 
				
			||||||
        .thenReturn(CompletableFuture.completedFuture(null));
 | 
					        .thenReturn(CompletableFuture.completedFuture(null));
 | 
				
			||||||
    final MediaEncryptionParameters encryptionParams = new MediaEncryptionParameters(
 | 
					    final MediaEncryptionParameters encryptionParams = new MediaEncryptionParameters(
 | 
				
			||||||
| 
						 | 
					@ -343,7 +378,7 @@ public class BackupManagerTest {
 | 
				
			||||||
  public void copyFailure() {
 | 
					  public void copyFailure() {
 | 
				
			||||||
    final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
 | 
					    final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupTier.MEDIA);
 | 
				
			||||||
    when(tusCredentialGenerator.generateUpload(any()))
 | 
					    when(tusCredentialGenerator.generateUpload(any()))
 | 
				
			||||||
        .thenReturn(new MessageBackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
 | 
					        .thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
 | 
				
			||||||
    when(remoteStorageManager.copy(eq(URI.create("cdn3.example.org/attachments/abc")), eq(100), any(), any()))
 | 
					    when(remoteStorageManager.copy(eq(URI.create("cdn3.example.org/attachments/abc")), eq(100), any(), any()))
 | 
				
			||||||
        .thenReturn(CompletableFuture.failedFuture(new SourceObjectNotFoundException()));
 | 
					        .thenReturn(CompletableFuture.failedFuture(new SourceObjectNotFoundException()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ public class Cdn3BackupCredentialGeneratorTest {
 | 
				
			||||||
        new SecretBytes(TestRandomUtil.nextBytes(32)),
 | 
					        new SecretBytes(TestRandomUtil.nextBytes(32)),
 | 
				
			||||||
        "https://example.org/upload"));
 | 
					        "https://example.org/upload"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final MessageBackupUploadDescriptor messageBackupUploadDescriptor = generator.generateUpload("subdir/key");
 | 
					    final BackupUploadDescriptor messageBackupUploadDescriptor = generator.generateUpload("subdir/key");
 | 
				
			||||||
    assertThat(messageBackupUploadDescriptor.signedUploadLocation()).isEqualTo("https://example.org/upload/backups");
 | 
					    assertThat(messageBackupUploadDescriptor.signedUploadLocation()).isEqualTo("https://example.org/upload/backups");
 | 
				
			||||||
    assertThat(messageBackupUploadDescriptor.key()).isEqualTo("subdir/key");
 | 
					    assertThat(messageBackupUploadDescriptor.key()).isEqualTo("subdir/key");
 | 
				
			||||||
    assertThat(messageBackupUploadDescriptor.headers()).containsKey("Authorization");
 | 
					    assertThat(messageBackupUploadDescriptor.headers()).containsKey("Authorization");
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -124,7 +124,7 @@ public class Cdn3RemoteStorageManagerTest {
 | 
				
			||||||
            URI.create(wireMock.url("/cdn" + sourceCdn + "/source/small")),
 | 
					            URI.create(wireMock.url("/cdn" + sourceCdn + "/source/small")),
 | 
				
			||||||
            expectedSource.length(),
 | 
					            expectedSource.length(),
 | 
				
			||||||
            encryptionParameters,
 | 
					            encryptionParameters,
 | 
				
			||||||
            new MessageBackupUploadDescriptor(3, "test", Collections.emptyMap(), wireMock.url("/cdn3/dest")))
 | 
					            new BackupUploadDescriptor(3, "test", Collections.emptyMap(), wireMock.url("/cdn3/dest")))
 | 
				
			||||||
        .toCompletableFuture().join();
 | 
					        .toCompletableFuture().join();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final byte[] destBody = wireMock.findAll(postRequestedFor(urlEqualTo("/cdn3/dest"))).get(0).getBody();
 | 
					    final byte[] destBody = wireMock.findAll(postRequestedFor(urlEqualTo("/cdn3/dest"))).get(0).getBody();
 | 
				
			||||||
| 
						 | 
					@ -148,7 +148,7 @@ public class Cdn3RemoteStorageManagerTest {
 | 
				
			||||||
            URI.create(wireMock.url("/cdn3/source/large")),
 | 
					            URI.create(wireMock.url("/cdn3/source/large")),
 | 
				
			||||||
            LARGE.length(),
 | 
					            LARGE.length(),
 | 
				
			||||||
            params,
 | 
					            params,
 | 
				
			||||||
            new MessageBackupUploadDescriptor(3, "test", Collections.emptyMap(), wireMock.url("/cdn3/dest")))
 | 
					            new BackupUploadDescriptor(3, "test", Collections.emptyMap(), wireMock.url("/cdn3/dest")))
 | 
				
			||||||
        .toCompletableFuture().join();
 | 
					        .toCompletableFuture().join();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final byte[] destBody = wireMock.findAll(postRequestedFor(urlEqualTo("/cdn3/dest"))).get(0).getBody();
 | 
					    final byte[] destBody = wireMock.findAll(postRequestedFor(urlEqualTo("/cdn3/dest"))).get(0).getBody();
 | 
				
			||||||
| 
						 | 
					@ -165,7 +165,7 @@ public class Cdn3RemoteStorageManagerTest {
 | 
				
			||||||
                URI.create(wireMock.url("/cdn3/source/small")),
 | 
					                URI.create(wireMock.url("/cdn3/source/small")),
 | 
				
			||||||
                SMALL_CDN3.length() - 1,
 | 
					                SMALL_CDN3.length() - 1,
 | 
				
			||||||
                new MediaEncryptionParameters(AES_KEY, HMAC_KEY, IV),
 | 
					                new MediaEncryptionParameters(AES_KEY, HMAC_KEY, IV),
 | 
				
			||||||
                new MessageBackupUploadDescriptor(3, "test", Collections.emptyMap(), wireMock.url("/cdn3/dest")))
 | 
					                new BackupUploadDescriptor(3, "test", Collections.emptyMap(), wireMock.url("/cdn3/dest")))
 | 
				
			||||||
            .toCompletableFuture());
 | 
					            .toCompletableFuture());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -176,7 +176,7 @@ public class Cdn3RemoteStorageManagerTest {
 | 
				
			||||||
                URI.create(wireMock.url("/cdn3/source/missing")),
 | 
					                URI.create(wireMock.url("/cdn3/source/missing")),
 | 
				
			||||||
                1,
 | 
					                1,
 | 
				
			||||||
                new MediaEncryptionParameters(AES_KEY, HMAC_KEY, IV),
 | 
					                new MediaEncryptionParameters(AES_KEY, HMAC_KEY, IV),
 | 
				
			||||||
                new MessageBackupUploadDescriptor(3, "test", Collections.emptyMap(), wireMock.url("/cdn3/dest")))
 | 
					                new BackupUploadDescriptor(3, "test", Collections.emptyMap(), wireMock.url("/cdn3/dest")))
 | 
				
			||||||
            .toCompletableFuture());
 | 
					            .toCompletableFuture());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,7 @@ import java.time.temporal.ChronoUnit;
 | 
				
			||||||
import java.util.Arrays;
 | 
					import java.util.Arrays;
 | 
				
			||||||
import java.util.Base64;
 | 
					import java.util.Base64;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
import java.util.Optional;
 | 
					import java.util.Optional;
 | 
				
			||||||
import java.util.UUID;
 | 
					import java.util.UUID;
 | 
				
			||||||
import java.util.concurrent.CompletableFuture;
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
| 
						 | 
					@ -36,7 +37,6 @@ import javax.ws.rs.client.Invocation;
 | 
				
			||||||
import javax.ws.rs.client.WebTarget;
 | 
					import javax.ws.rs.client.WebTarget;
 | 
				
			||||||
import javax.ws.rs.core.MediaType;
 | 
					import javax.ws.rs.core.MediaType;
 | 
				
			||||||
import javax.ws.rs.core.Response;
 | 
					import javax.ws.rs.core.Response;
 | 
				
			||||||
import org.checkerframework.checker.units.qual.A;
 | 
					 | 
				
			||||||
import org.glassfish.jersey.server.ServerProperties;
 | 
					import org.glassfish.jersey.server.ServerProperties;
 | 
				
			||||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
 | 
					import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
 | 
				
			||||||
import org.junit.jupiter.api.BeforeEach;
 | 
					import org.junit.jupiter.api.BeforeEach;
 | 
				
			||||||
| 
						 | 
					@ -68,6 +68,7 @@ import org.whispersystems.textsecuregcm.backup.BackupManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.backup.BackupTier;
 | 
					import org.whispersystems.textsecuregcm.backup.BackupTier;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.backup.InvalidLengthException;
 | 
					import org.whispersystems.textsecuregcm.backup.InvalidLengthException;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.backup.SourceObjectNotFoundException;
 | 
					import org.whispersystems.textsecuregcm.backup.SourceObjectNotFoundException;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.backup.BackupUploadDescriptor;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
 | 
					import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.mappers.GrpcStatusRuntimeExceptionMapper;
 | 
					import org.whispersystems.textsecuregcm.mappers.GrpcStatusRuntimeExceptionMapper;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
 | 
					import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
 | 
				
			||||||
| 
						 | 
					@ -108,6 +109,7 @@ public class ArchiveControllerTest {
 | 
				
			||||||
      GET,    v1/archives/auth/read,
 | 
					      GET,    v1/archives/auth/read,
 | 
				
			||||||
      GET,    v1/archives/,
 | 
					      GET,    v1/archives/,
 | 
				
			||||||
      GET,    v1/archives/upload/form,
 | 
					      GET,    v1/archives/upload/form,
 | 
				
			||||||
 | 
					      GET,    v1/archives/media/upload/form,
 | 
				
			||||||
      POST,   v1/archives/,
 | 
					      POST,   v1/archives/,
 | 
				
			||||||
      PUT,    v1/archives/keys, '{"backupIdPublicKey": "aaaaa"}'
 | 
					      PUT,    v1/archives/keys, '{"backupIdPublicKey": "aaaaa"}'
 | 
				
			||||||
      PUT,    v1/archives/media, '{
 | 
					      PUT,    v1/archives/media, '{
 | 
				
			||||||
| 
						 | 
					@ -531,6 +533,38 @@ public class ArchiveControllerTest {
 | 
				
			||||||
    assertThat(response.getStatus()).isEqualTo(204);
 | 
					    assertThat(response.getStatus()).isEqualTo(204);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  public void mediaUploadForm() throws RateLimitExceededException, VerificationFailedException {
 | 
				
			||||||
 | 
					    final BackupAuthCredentialPresentation presentation =
 | 
				
			||||||
 | 
					        backupAuthTestUtil.getPresentation(BackupTier.MEDIA, backupKey, aci);
 | 
				
			||||||
 | 
					    when(backupManager.authenticateBackupUser(any(), any()))
 | 
				
			||||||
 | 
					        .thenReturn(CompletableFuture.completedFuture(backupUser(presentation.getBackupId(), BackupTier.MEDIA)));
 | 
				
			||||||
 | 
					    when(backupManager.createTemporaryAttachmentUploadDescriptor(any()))
 | 
				
			||||||
 | 
					        .thenReturn(new BackupUploadDescriptor(3, "abc", Map.of("k", "v"), "example.org"));
 | 
				
			||||||
 | 
					    final ArchiveController.UploadDescriptorResponse desc = resources.getJerseyTest()
 | 
				
			||||||
 | 
					        .target("v1/archives/media/upload/form")
 | 
				
			||||||
 | 
					        .request()
 | 
				
			||||||
 | 
					        .header("X-Signal-ZK-Auth", Base64.getEncoder().encodeToString(presentation.serialize()))
 | 
				
			||||||
 | 
					        .header("X-Signal-ZK-Auth-Signature", "aaa")
 | 
				
			||||||
 | 
					        .get(ArchiveController.UploadDescriptorResponse.class);
 | 
				
			||||||
 | 
					    assertThat(desc.cdn()).isEqualTo(3);
 | 
				
			||||||
 | 
					    assertThat(desc.key()).isEqualTo("abc");
 | 
				
			||||||
 | 
					    assertThat(desc.headers()).containsExactlyEntriesOf(Map.of("k", "v"));
 | 
				
			||||||
 | 
					    assertThat(desc.signedUploadLocation()).isEqualTo("example.org");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // rate limit
 | 
				
			||||||
 | 
					    when(backupManager.createTemporaryAttachmentUploadDescriptor(any()))
 | 
				
			||||||
 | 
					        .thenThrow(new RateLimitExceededException(null, false));
 | 
				
			||||||
 | 
					    final Response response = resources.getJerseyTest()
 | 
				
			||||||
 | 
					        .target("v1/archives/media/upload/form")
 | 
				
			||||||
 | 
					        .request()
 | 
				
			||||||
 | 
					        .header("X-Signal-ZK-Auth", Base64.getEncoder().encodeToString(presentation.serialize()))
 | 
				
			||||||
 | 
					        .header("X-Signal-ZK-Auth-Signature", "aaa")
 | 
				
			||||||
 | 
					        .get();
 | 
				
			||||||
 | 
					    assertThat(response.getStatus()).isEqualTo(429);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static AuthenticatedBackupUser backupUser(byte[] backupId, BackupTier backupTier) {
 | 
					  private static AuthenticatedBackupUser backupUser(byte[] backupId, BackupTier backupTier) {
 | 
				
			||||||
    return new AuthenticatedBackupUser(backupId, backupTier, "myBackupDir", "myMediaDir");
 | 
					    return new AuthenticatedBackupUser(backupId, backupTier, "myBackupDir", "myMediaDir");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue