mirror of
https://github.com/signalapp/Signal-Server
synced 2026-02-15 09:35:41 +00:00
Convert backup services to use new error model
This commit is contained in:
@@ -166,6 +166,7 @@ import org.whispersystems.textsecuregcm.limits.RateLimitByIpFilter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitChallengeManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.limits.RedisMessageDeliveryLoopMonitor;
|
||||
import org.whispersystems.textsecuregcm.mappers.BackupExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.GrpcStatusRuntimeExceptionMapper;
|
||||
@@ -1200,6 +1201,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new ObsoletePhoneNumberFormatExceptionMapper(),
|
||||
new RegistrationServiceSenderExceptionMapper(),
|
||||
new SubscriptionExceptionMapper(),
|
||||
new BackupExceptionMapper(),
|
||||
new JsonMappingExceptionMapper()
|
||||
).forEach(exceptionMapper -> {
|
||||
environment.jersey().register(exceptionMapper);
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import io.grpc.Status;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
@@ -98,15 +99,14 @@ public class BackupAuthManager {
|
||||
final Account account,
|
||||
final Device device,
|
||||
final Optional<BackupAuthCredentialRequest> messagesBackupCredentialRequest,
|
||||
final Optional<BackupAuthCredentialRequest> mediaBackupCredentialRequest) throws RateLimitExceededException {
|
||||
final Optional<BackupAuthCredentialRequest> mediaBackupCredentialRequest)
|
||||
throws RateLimitExceededException, BackupPermissionException, BackupInvalidArgumentException {
|
||||
if (!device.isPrimary()) {
|
||||
throw Status.PERMISSION_DENIED.withDescription("Only primary device can set backup-id").asRuntimeException();
|
||||
throw new BackupPermissionException("Only primary device can set backup-id");
|
||||
}
|
||||
|
||||
if (messagesBackupCredentialRequest.isEmpty() && mediaBackupCredentialRequest.isEmpty()) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("Must set at least one of message/media credential requests")
|
||||
.asRuntimeException();
|
||||
throw new BackupInvalidArgumentException("Must set at least one of message/media credential requests");
|
||||
}
|
||||
|
||||
final byte[] storedMessageCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MESSAGES)
|
||||
@@ -191,7 +191,7 @@ public class BackupAuthManager {
|
||||
public List<Credential> getBackupAuthCredentials(
|
||||
final Account account,
|
||||
final BackupCredentialType credentialType,
|
||||
final RedemptionRange redemptionRange) {
|
||||
final RedemptionRange redemptionRange) throws BackupNotFoundException {
|
||||
|
||||
// If the account has an expired payment, clear it before continuing
|
||||
if (hasExpiredVoucher(account)) {
|
||||
@@ -206,7 +206,7 @@ public class BackupAuthManager {
|
||||
|
||||
// fetch the blinded backup-id the account should have previously committed to
|
||||
final byte[] committedBytes = account.getBackupCredentialRequest(credentialType)
|
||||
.orElseThrow(() -> Status.NOT_FOUND.withDescription("No blinded backup-id has been added to the account").asRuntimeException());
|
||||
.orElseThrow(() -> new BackupNotFoundException("No blinded backup-id has been added to the account"));
|
||||
|
||||
try {
|
||||
final BackupLevel defaultBackupLevel = configuredBackupLevel(account);
|
||||
@@ -224,10 +224,7 @@ public class BackupAuthManager {
|
||||
})
|
||||
.toList();
|
||||
} catch (InvalidInputException e) {
|
||||
throw Status.INTERNAL
|
||||
.withDescription("Could not deserialize stored request credential")
|
||||
.withCause(e)
|
||||
.asRuntimeException();
|
||||
throw new UncheckedIOException(new IOException("Could not deserialize stored request credential", e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,41 +236,34 @@ public class BackupAuthManager {
|
||||
*/
|
||||
public void redeemReceipt(
|
||||
final Account account,
|
||||
final ReceiptCredentialPresentation receiptCredentialPresentation) {
|
||||
final ReceiptCredentialPresentation receiptCredentialPresentation)
|
||||
throws BackupBadReceiptException, BackupInvalidArgumentException, BackupMissingIdCommitmentException {
|
||||
try {
|
||||
serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);
|
||||
} catch (VerificationFailedException e) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("receipt credential presentation verification failed")
|
||||
.asRuntimeException();
|
||||
throw new BackupBadReceiptException("receipt credential presentation verification failed");
|
||||
}
|
||||
final ReceiptSerial receiptSerial = receiptCredentialPresentation.getReceiptSerial();
|
||||
final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());
|
||||
if (clock.instant().isAfter(receiptExpiration)) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("receipt is already expired").asRuntimeException();
|
||||
throw new BackupBadReceiptException("receipt is already expired");
|
||||
}
|
||||
|
||||
final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();
|
||||
|
||||
if (BackupLevelUtil.fromReceiptLevel(receiptLevel) != BackupLevel.PAID) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("server does not recognize the requested receipt level")
|
||||
.asRuntimeException();
|
||||
throw new BackupInvalidArgumentException("server does not recognize the requested receipt level");
|
||||
}
|
||||
|
||||
if (account.getBackupCredentialRequest(BackupCredentialType.MEDIA).isEmpty()) {
|
||||
throw Status.ABORTED
|
||||
.withDescription("account must have a backup-id commitment")
|
||||
.asRuntimeException();
|
||||
throw new BackupMissingIdCommitmentException();
|
||||
}
|
||||
|
||||
boolean receiptAllowed = redeemedReceiptsManager
|
||||
.put(receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, account.getUuid())
|
||||
.join();
|
||||
if (!receiptAllowed) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("receipt serial is already redeemed")
|
||||
.asRuntimeException();
|
||||
throw new BackupBadReceiptException("receipt serial is already redeemed");
|
||||
}
|
||||
extendBackupVoucher(account, new Account.BackupVoucher(receiptLevel, receiptExpiration));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
public class BackupBadReceiptException extends BackupException {
|
||||
|
||||
public BackupBadReceiptException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
public class BackupException extends Exception {
|
||||
|
||||
public BackupException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public BackupException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
public class BackupFailedZkAuthenticationException extends BackupException {
|
||||
|
||||
public BackupFailedZkAuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
public class BackupInvalidArgumentException extends BackupException {
|
||||
public BackupInvalidArgumentException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.dropwizard.util.DataSize;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import io.micrometer.core.instrument.DistributionSummary;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
@@ -128,11 +126,12 @@ public class BackupManager {
|
||||
* @param presentation a ZK credential presentation that encodes the backupId
|
||||
* @param signature the signature of the presentation
|
||||
* @param publicKey the public key of a key-pair that the presentation must be signed with
|
||||
* @throws BackupFailedZkAuthenticationException If the provided presentation or signature were invalid
|
||||
*/
|
||||
public void setPublicKey(
|
||||
final BackupAuthCredentialPresentation presentation,
|
||||
final byte[] signature,
|
||||
final ECPublicKey publicKey) {
|
||||
final ECPublicKey publicKey) throws BackupFailedZkAuthenticationException {
|
||||
|
||||
// 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
|
||||
@@ -141,16 +140,14 @@ public class BackupManager {
|
||||
verifyPresentation(presentation).verifySignature(signature, publicKey);
|
||||
|
||||
ExceptionUtils.unwrapSupply(
|
||||
PublicKeyConflictException.class,
|
||||
BackupPublicKeyConflictException.class,
|
||||
() -> backupsDb.setPublicKey(presentation.getBackupId(), credentialTypeAndBackupLevel.second(), publicKey).join(),
|
||||
_ -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
FAILURE_REASON_TAG_NAME, "public_key_conflict")
|
||||
.increment();
|
||||
throw Status.UNAUTHENTICATED
|
||||
.withDescription("public key does not match existing public key for the backup-id")
|
||||
.asRuntimeException();
|
||||
return new BackupFailedZkAuthenticationException("The provided public key did not match the stored public key");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,9 +158,11 @@ public class BackupManager {
|
||||
*
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
* @return the upload form
|
||||
* @throws BackupPermissionException if the credential does not have the correct level
|
||||
* @throws BackupWrongCredentialTypeException if the credential does not have the messages type
|
||||
*/
|
||||
public BackupUploadDescriptor createMessageBackupUploadDescriptor(
|
||||
final AuthenticatedBackupUser backupUser) {
|
||||
final AuthenticatedBackupUser backupUser) throws BackupPermissionException, BackupWrongCredentialTypeException {
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
checkBackupCredentialType(backupUser, BackupCredentialType.MESSAGES);
|
||||
|
||||
@@ -173,7 +172,7 @@ public class BackupManager {
|
||||
}
|
||||
|
||||
public BackupUploadDescriptor createTemporaryAttachmentUploadDescriptor(final AuthenticatedBackupUser backupUser)
|
||||
throws RateLimitExceededException {
|
||||
throws RateLimitExceededException, BackupPermissionException, BackupWrongCredentialTypeException {
|
||||
checkBackupLevel(backupUser, BackupLevel.PAID);
|
||||
checkBackupCredentialType(backupUser, BackupCredentialType.MEDIA);
|
||||
|
||||
@@ -189,8 +188,9 @@ public class BackupManager {
|
||||
* Update the last update timestamps for the backupId in the presentation
|
||||
*
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
* @throws BackupPermissionException if the credential does not have the correct level
|
||||
*/
|
||||
public void ttlRefresh(final AuthenticatedBackupUser backupUser) {
|
||||
public void ttlRefresh(final AuthenticatedBackupUser backupUser) throws BackupPermissionException {
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
// update message backup TTL
|
||||
final StoredBackupAttributes storedBackupAttributes = backupsDb.ttlRefresh(backupUser).join();
|
||||
@@ -229,10 +229,15 @@ public class BackupManager {
|
||||
*
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
* @return Information about the existing backup
|
||||
* @throws BackupPermissionException if the credential does not have the correct level
|
||||
* @throws BackupNotFoundException if the provided backupuser does not exist
|
||||
*/
|
||||
public BackupInfo backupInfo(final AuthenticatedBackupUser backupUser) {
|
||||
public BackupInfo backupInfo(final AuthenticatedBackupUser backupUser)
|
||||
throws BackupNotFoundException, BackupPermissionException {
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
final BackupsDb.BackupDescription backupDescription = backupsDb.describeBackup(backupUser).join();
|
||||
final BackupsDb.BackupDescription backupDescription = ExceptionUtils.unwrapSupply(
|
||||
BackupNotFoundException.class,
|
||||
() -> backupsDb.describeBackup(backupUser).join());
|
||||
return new BackupInfo(
|
||||
backupDescription.cdn(),
|
||||
backupUser.backupDir(),
|
||||
@@ -253,46 +258,42 @@ public class BackupManager {
|
||||
* @return A Flux that emits the locations of the double-encrypted objects on the backup cdn, or includes an error
|
||||
* detailing why the object could not be copied.
|
||||
*/
|
||||
public Flux<CopyResult> copyToBackup(final AuthenticatedBackupUser backupUser, List<CopyParameters> toCopy) {
|
||||
checkBackupLevel(backupUser, BackupLevel.PAID);
|
||||
checkBackupCredentialType(backupUser, BackupCredentialType.MEDIA);
|
||||
|
||||
public Flux<CopyResult> copyToBackup(final CopyQuota copyQuota) {
|
||||
final AuthenticatedBackupUser backupUser = copyQuota.backupUser();
|
||||
final DynamicBackupConfiguration backupConfiguration =
|
||||
dynamicConfigurationManager.getConfiguration().getBackupConfiguration();
|
||||
|
||||
return Mono.fromFuture(() -> allowedCopies(backupUser, toCopy))
|
||||
.flatMapMany(quotaResult -> Flux.concat(
|
||||
return Flux.concat(
|
||||
|
||||
// Perform copies for requests that fit in our quota, first updating the usage. If the copy fails, our
|
||||
// estimated quota usage may not be exact since we update usage first. We make a best-effort attempt
|
||||
// to undo the usage update if we know that the copied failed for sure.
|
||||
Flux.fromIterable(quotaResult.requestsToCopy())
|
||||
// Perform copies for requests that fit in our quota, first updating the usage. If the copy fails, our
|
||||
// estimated quota usage may not be exact since we update usage first. We make a best-effort attempt
|
||||
// to undo the usage update if we know that the copied failed for sure.
|
||||
Flux.fromIterable(copyQuota.requestsToCopy())
|
||||
|
||||
// Update the usage in reasonable chunk sizes to bound how out of sync our claimed and actual usage gets
|
||||
.buffer(backupConfiguration.usageCheckpointCount())
|
||||
.concatMap(copyParameters -> {
|
||||
final long quotaToConsume = copyParameters.stream()
|
||||
.mapToLong(CopyParameters::destinationObjectSize)
|
||||
.sum();
|
||||
return Mono
|
||||
.fromFuture(backupsDb.trackMedia(backupUser, copyParameters.size(), quotaToConsume))
|
||||
.thenMany(Flux.fromIterable(copyParameters));
|
||||
})
|
||||
// Update the usage in reasonable chunk sizes to bound how out of sync our claimed and actual usage gets
|
||||
.buffer(backupConfiguration.usageCheckpointCount())
|
||||
.concatMap(copyParameters -> {
|
||||
final long quotaToConsume = copyParameters.stream()
|
||||
.mapToLong(CopyParameters::destinationObjectSize)
|
||||
.sum();
|
||||
return Mono
|
||||
.fromFuture(backupsDb.trackMedia(backupUser, copyParameters.size(), quotaToConsume))
|
||||
.thenMany(Flux.fromIterable(copyParameters));
|
||||
})
|
||||
|
||||
// Actually perform the copies now that we've updated the quota
|
||||
.flatMapSequential(copyParams -> copyToBackup(backupUser, copyParams)
|
||||
.flatMap(copyResult -> switch (copyResult.outcome()) {
|
||||
case SUCCESS -> Mono.just(copyResult);
|
||||
case SOURCE_WRONG_LENGTH, SOURCE_NOT_FOUND, OUT_OF_QUOTA -> Mono
|
||||
.fromFuture(this.backupsDb.trackMedia(backupUser, -1, -copyParams.destinationObjectSize()))
|
||||
.thenReturn(copyResult);
|
||||
}),
|
||||
backupConfiguration.copyConcurrency(), 1),
|
||||
// Actually perform the copies now that we've updated the quota
|
||||
.flatMapSequential(copyParams -> copyToBackup(backupUser, copyParams)
|
||||
.flatMap(copyResult -> switch (copyResult.outcome()) {
|
||||
case SUCCESS -> Mono.just(copyResult);
|
||||
case SOURCE_WRONG_LENGTH, SOURCE_NOT_FOUND, OUT_OF_QUOTA -> Mono
|
||||
.fromFuture(this.backupsDb.trackMedia(backupUser, -1, -copyParams.destinationObjectSize()))
|
||||
.thenReturn(copyResult);
|
||||
}),
|
||||
backupConfiguration.copyConcurrency(), 1),
|
||||
|
||||
// There wasn't enough quota remaining to perform these copies
|
||||
Flux.fromIterable(quotaResult.requestsToReject())
|
||||
.map(arg -> new CopyResult(CopyResult.Outcome.OUT_OF_QUOTA, arg.destinationMediaId(), null))
|
||||
));
|
||||
// There wasn't enough quota remaining to perform these copies
|
||||
Flux.fromIterable(copyQuota.requestsToReject())
|
||||
.map(arg -> new CopyResult(CopyResult.Outcome.OUT_OF_QUOTA, arg.destinationMediaId(), null)));
|
||||
}
|
||||
|
||||
private Mono<CopyResult> copyToBackup(final AuthenticatedBackupUser backupUser, final CopyParameters copyParameters) {
|
||||
@@ -315,64 +316,67 @@ public class BackupManager {
|
||||
Mono.just(CopyResult.fromCopyError(throwable, copyParameters.destinationMediaId()).orElseThrow()));
|
||||
}
|
||||
|
||||
private record QuotaResult(List<CopyParameters> requestsToCopy, List<CopyParameters> requestsToReject) {}
|
||||
public record CopyQuota(AuthenticatedBackupUser backupUser, List<CopyParameters> requestsToCopy, List<CopyParameters> requestsToReject) {
|
||||
private static CopyQuota create(AuthenticatedBackupUser backupUser, final List<CopyParameters> toCopy, long remainingQuota) {
|
||||
// Figure out how many of the requested objects fit in the remaining quota
|
||||
final int index = indexWhereTotalExceeds(toCopy, CopyParameters::destinationObjectSize, remainingQuota);
|
||||
return new CopyQuota(backupUser, toCopy.subList(0, index), toCopy.subList(index, toCopy.size()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which copy requests can be performed with the user's remaining quota. This does not update the quota.
|
||||
*
|
||||
* @param backupUser The user quota to check against
|
||||
* @param toCopy The proposed copy requests
|
||||
* @return list of QuotaResult indicating which requests fit into the remaining quota and which requests should be
|
||||
* @return QuotaResult indicating which requests fit into the remaining quota and which requests should be
|
||||
* rejected with {@link CopyResult.Outcome#OUT_OF_QUOTA}
|
||||
* @throws BackupInvalidArgumentException if toCopy contains an invalid copy request
|
||||
* @throws BackupPermissionException if the credential does not have the correct level
|
||||
* @throws BackupWrongCredentialTypeException if the credential does not have the media type
|
||||
*/
|
||||
private CompletableFuture<QuotaResult> allowedCopies(
|
||||
public CopyQuota getCopyQuota(
|
||||
final AuthenticatedBackupUser backupUser,
|
||||
final List<CopyParameters> toCopy) {
|
||||
final long totalBytesAdded = toCopy.stream()
|
||||
.mapToLong(copyParameters -> {
|
||||
if (copyParameters.sourceLength() > MAX_MEDIA_OBJECT_SIZE || copyParameters.sourceLength() < 0) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("Invalid sourceObject size")
|
||||
.asRuntimeException();
|
||||
}
|
||||
return copyParameters.destinationObjectSize();
|
||||
})
|
||||
.sum();
|
||||
final List<CopyParameters> toCopy)
|
||||
throws BackupWrongCredentialTypeException, BackupPermissionException, BackupInvalidArgumentException {
|
||||
checkBackupLevel(backupUser, BackupLevel.PAID);
|
||||
checkBackupCredentialType(backupUser, BackupCredentialType.MEDIA);
|
||||
|
||||
for (CopyParameters copyParameters : toCopy) {
|
||||
if (copyParameters.sourceLength() > MAX_MEDIA_OBJECT_SIZE || copyParameters.sourceLength() < 0) {
|
||||
throw new BackupInvalidArgumentException("Invalid sourceObject size");
|
||||
}
|
||||
}
|
||||
final long totalBytesAdded = toCopy.stream().mapToLong(CopyParameters::destinationObjectSize).sum();
|
||||
|
||||
final DynamicBackupConfiguration backupConfiguration =
|
||||
dynamicConfigurationManager.getConfiguration().getBackupConfiguration();
|
||||
final Duration maxQuotaStaleness = backupConfiguration.maxQuotaStaleness();
|
||||
final long maxTotalMediaSize = backupConfiguration.maxTotalMediaSize();
|
||||
|
||||
return backupsDb.getMediaUsage(backupUser)
|
||||
.thenComposeAsync(info -> {
|
||||
long remainingQuota = maxTotalMediaSize - info.usageInfo().bytesUsed();
|
||||
final boolean canStore = remainingQuota >= totalBytesAdded;
|
||||
if (canStore || info.lastRecalculationTime().isAfter(clock.instant().minus(maxQuotaStaleness))) {
|
||||
return CompletableFuture.completedFuture(remainingQuota);
|
||||
}
|
||||
final BackupsDb.TimestampedUsageInfo info = backupsDb.getMediaUsage(backupUser).join();
|
||||
long estimatedRemainingQuota = maxTotalMediaSize - info.usageInfo().bytesUsed();
|
||||
final boolean canStore = estimatedRemainingQuota >= totalBytesAdded;
|
||||
if (canStore || info.lastRecalculationTime().isAfter(clock.instant().minus(maxQuotaStaleness))) {
|
||||
return CopyQuota.create(backupUser, toCopy, estimatedRemainingQuota);
|
||||
}
|
||||
|
||||
// The user is out of quota, and we have not recently recalculated the user's usage. Double check by doing a
|
||||
// hard recalculation before actually forbidding the user from storing additional media.
|
||||
return this.remoteStorageManager.calculateBytesUsed(cdnMediaDirectory(backupUser))
|
||||
.thenCompose(usage -> backupsDb
|
||||
.setMediaUsage(backupUser, usage)
|
||||
.thenApply(ignored -> usage))
|
||||
.whenComplete((newUsage, throwable) -> {
|
||||
boolean usageChanged = throwable == null && !newUsage.equals(info.usageInfo());
|
||||
Metrics.counter(USAGE_RECALCULATION_COUNTER_NAME, Tags.of(
|
||||
UserAgentTagUtil.getPlatformTag(backupUser.userAgent()),
|
||||
Tag.of("usageChanged", String.valueOf(usageChanged))))
|
||||
.increment();
|
||||
})
|
||||
.thenApply(newUsage -> maxTotalMediaSize - newUsage.bytesUsed());
|
||||
})
|
||||
.thenApply(remainingQuota -> {
|
||||
// Figure out how many of the requested objects fit in the remaining quota
|
||||
final int index = indexWhereTotalExceeds(toCopy, CopyParameters::destinationObjectSize,
|
||||
remainingQuota);
|
||||
return new QuotaResult(toCopy.subList(0, index), toCopy.subList(index, toCopy.size()));
|
||||
});
|
||||
// The user is out of quota, and we have not recently recalculated the user's usage. Double check by doing a
|
||||
// hard recalculation before actually forbidding the user from storing additional media.
|
||||
boolean usageChanged = false;
|
||||
try {
|
||||
final UsageInfo usage =
|
||||
this.remoteStorageManager.calculateBytesUsed(cdnMediaDirectory(backupUser)).toCompletableFuture().join();
|
||||
backupsDb.setMediaUsage(backupUser, usage).join();
|
||||
usageChanged = !usage.equals(info.usageInfo());
|
||||
|
||||
final long remainingQuota = maxTotalMediaSize - usage.bytesUsed();
|
||||
return CopyQuota.create(backupUser, toCopy, remainingQuota);
|
||||
} finally {
|
||||
Metrics.counter(USAGE_RECALCULATION_COUNTER_NAME, Tags.of(
|
||||
UserAgentTagUtil.getPlatformTag(backupUser.userAgent()),
|
||||
Tag.of("usageChanged", String.valueOf(usageChanged))))
|
||||
.increment();
|
||||
}
|
||||
}
|
||||
|
||||
public record RecalculationResult(UsageInfo oldUsage, UsageInfo newUsage) {}
|
||||
@@ -413,11 +417,14 @@ public class BackupManager {
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
* @param cdnNumber the cdn number to get backup credentials for
|
||||
* @return A map of headers to include with CDN requests
|
||||
* @throws BackupPermissionException if the credential does not have the correct level
|
||||
* @throws BackupInvalidArgumentException if the provided cdnNumber is invalid
|
||||
*/
|
||||
public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backupUser, final int cdnNumber) {
|
||||
public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backupUser, final int cdnNumber)
|
||||
throws BackupInvalidArgumentException, BackupPermissionException {
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
if (cdnNumber != 3) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("unknown cdn").asRuntimeException();
|
||||
throw new BackupInvalidArgumentException("unknown cdn");
|
||||
}
|
||||
return cdn3BackupCredentialGenerator.readHeaders(backupUser.backupDir());
|
||||
}
|
||||
@@ -427,8 +434,11 @@ public class BackupManager {
|
||||
*
|
||||
* @param backupUser an already ZK authenticated backup user
|
||||
* @return the credential that may be used with SVRB
|
||||
* @throws BackupPermissionException if the credential does not have the correct level
|
||||
* @throws BackupWrongCredentialTypeException if the credential does not have the messages type
|
||||
*/
|
||||
public ExternalServiceCredentials generateSvrbAuth(final AuthenticatedBackupUser backupUser) {
|
||||
public ExternalServiceCredentials generateSvrbAuth(final AuthenticatedBackupUser backupUser)
|
||||
throws BackupPermissionException, BackupWrongCredentialTypeException {
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
// Clients may only use SVRB with their messages backup-id
|
||||
checkBackupCredentialType(backupUser, BackupCredentialType.MESSAGES);
|
||||
@@ -458,11 +468,12 @@ public class BackupManager {
|
||||
* @param cursor A cursor returned by a previous call that can be used to resume listing
|
||||
* @param limit The maximum number of list results to return
|
||||
* @return A {@link ListMediaResult}
|
||||
* @throws BackupPermissionException if the credential does not have the correct level
|
||||
*/
|
||||
public ListMediaResult list(
|
||||
final AuthenticatedBackupUser backupUser,
|
||||
final Optional<String> cursor,
|
||||
final int limit) {
|
||||
final int limit) throws BackupPermissionException {
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
final RemoteStorageManager.ListResult result =
|
||||
remoteStorageManager.list(cdnMediaDirectory(backupUser), cursor, limit).toCompletableFuture().join();
|
||||
@@ -478,7 +489,7 @@ public class BackupManager {
|
||||
result.cursor());
|
||||
}
|
||||
|
||||
public void deleteEntireBackup(final AuthenticatedBackupUser backupUser) {
|
||||
public void deleteEntireBackup(final AuthenticatedBackupUser backupUser) throws BackupPermissionException {
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
|
||||
final int deletionConcurrency =
|
||||
@@ -504,15 +515,14 @@ public class BackupManager {
|
||||
|
||||
|
||||
public Flux<StorageDescriptor> deleteMedia(final AuthenticatedBackupUser backupUser,
|
||||
final List<StorageDescriptor> storageDescriptors) {
|
||||
final List<StorageDescriptor> storageDescriptors)
|
||||
throws BackupPermissionException, BackupWrongCredentialTypeException {
|
||||
checkBackupLevel(backupUser, BackupLevel.FREE);
|
||||
checkBackupCredentialType(backupUser, BackupCredentialType.MEDIA);
|
||||
|
||||
// Check for a cdn we don't know how to process
|
||||
if (storageDescriptors.stream().anyMatch(sd -> sd.cdn() != remoteStorageManager.cdnNumber())) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("unsupported media cdn provided")
|
||||
.asRuntimeException();
|
||||
return Flux.error(new BackupInvalidArgumentException("unsupported media cdn provided"));
|
||||
}
|
||||
final DynamicBackupConfiguration backupConfiguration =
|
||||
dynamicConfigurationManager.getConfiguration().getBackupConfiguration();
|
||||
@@ -614,65 +624,40 @@ public class BackupManager {
|
||||
* @param presentation A {@link BackupAuthCredentialPresentation}
|
||||
* @param signature An XEd25519 signature of the presentation bytes
|
||||
* @return On authentication success, the authenticated backup-id and backup-tier encoded in the presentation
|
||||
* @throws BackupFailedZkAuthenticationException If the provided presentation or signature were invalid
|
||||
*/
|
||||
public AuthenticatedBackupUser authenticateBackupUser(
|
||||
final BackupAuthCredentialPresentation presentation,
|
||||
final byte[] signature,
|
||||
final String userAgentString) {
|
||||
return ExceptionUtils.unwrapSupply(
|
||||
StatusRuntimeException.class,
|
||||
() -> authenticateBackupUserAsync(presentation, signature, userAgentString).join());
|
||||
}
|
||||
final String userAgentString) throws BackupFailedZkAuthenticationException {
|
||||
|
||||
/**
|
||||
* Authenticate the ZK anonymous backup credential's presentation
|
||||
* <p>
|
||||
* This validates:
|
||||
* <li> The presentation was for a credential issued by the server </li>
|
||||
* <li> The credential is in its redemption window </li>
|
||||
* <li> The backup-id matches a previously committed blinded backup-id and server issued receipt level </li>
|
||||
* <li> The signature of the credential matches an existing publicKey associated with this backup-id </li>
|
||||
*
|
||||
* @param presentation A {@link BackupAuthCredentialPresentation}
|
||||
* @param signature An XEd25519 signature of the presentation bytes
|
||||
* @return A future that completes with the authenticated backup-id and backup-tier encoded in the presentation
|
||||
*/
|
||||
public CompletableFuture<AuthenticatedBackupUser> authenticateBackupUserAsync(
|
||||
final BackupAuthCredentialPresentation presentation,
|
||||
final byte[] signature,
|
||||
final String userAgentString) {
|
||||
final PresentationSignatureVerifier signatureVerifier = verifyPresentation(presentation);
|
||||
|
||||
return backupsDb
|
||||
.retrieveAuthenticationData(presentation.getBackupId())
|
||||
.thenApply(optionalAuthenticationData -> {
|
||||
final UserAgent userAgent = parseUserAgent(userAgentString);
|
||||
final BackupsDb.AuthenticationData authenticationData = optionalAuthenticationData
|
||||
.orElseGet(() -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME, Tags.of(
|
||||
Tag.of(SUCCESS_TAG_NAME, String.valueOf(false)),
|
||||
Tag.of(FAILURE_REASON_TAG_NAME, "missing_public_key"),
|
||||
UserAgentTagUtil.getPlatformTag(userAgent)))
|
||||
.increment();
|
||||
// There was no stored public key, use a bunk public key so that validation will fail
|
||||
return new BackupsDb.AuthenticationData(INVALID_PUBLIC_KEY, null, null);
|
||||
});
|
||||
|
||||
final Pair<BackupCredentialType, BackupLevel> credentialTypeAndBackupLevel =
|
||||
signatureVerifier.verifySignature(signature, authenticationData.publicKey());
|
||||
|
||||
return new AuthenticatedBackupUser(
|
||||
presentation.getBackupId(),
|
||||
credentialTypeAndBackupLevel.first(),
|
||||
credentialTypeAndBackupLevel.second(),
|
||||
authenticationData.backupDir(),
|
||||
authenticationData.mediaDir(),
|
||||
userAgent);
|
||||
})
|
||||
.thenApply(result -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME, SUCCESS_TAG_NAME, String.valueOf(true)).increment();
|
||||
return result;
|
||||
final Optional<BackupsDb.AuthenticationData> optionalAuthenticationData =
|
||||
backupsDb.retrieveAuthenticationData(presentation.getBackupId()).join();
|
||||
final UserAgent userAgent = parseUserAgent(userAgentString);
|
||||
final BackupsDb.AuthenticationData authenticationData = optionalAuthenticationData
|
||||
.orElseGet(() -> {
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME, Tags.of(
|
||||
Tag.of(SUCCESS_TAG_NAME, String.valueOf(false)),
|
||||
Tag.of(FAILURE_REASON_TAG_NAME, "missing_public_key"),
|
||||
UserAgentTagUtil.getPlatformTag(userAgent)))
|
||||
.increment();
|
||||
// There was no stored public key, use a bunk public key so that validation will fail
|
||||
return new BackupsDb.AuthenticationData(INVALID_PUBLIC_KEY, null, null);
|
||||
});
|
||||
|
||||
final Pair<BackupCredentialType, BackupLevel> credentialTypeAndBackupLevel =
|
||||
signatureVerifier.verifySignature(signature, authenticationData.publicKey());
|
||||
|
||||
Metrics.counter(ZK_AUTHN_COUNTER_NAME, SUCCESS_TAG_NAME, String.valueOf(true)).increment();
|
||||
return new AuthenticatedBackupUser(
|
||||
presentation.getBackupId(),
|
||||
credentialTypeAndBackupLevel.first(),
|
||||
credentialTypeAndBackupLevel.second(),
|
||||
authenticationData.backupDir(),
|
||||
authenticationData.mediaDir(),
|
||||
userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -754,7 +739,7 @@ public class BackupManager {
|
||||
|
||||
interface PresentationSignatureVerifier {
|
||||
|
||||
Pair<BackupCredentialType, BackupLevel> verifySignature(byte[] signature, ECPublicKey publicKey);
|
||||
Pair<BackupCredentialType, BackupLevel> verifySignature(byte[] signature, ECPublicKey publicKey) throws BackupFailedZkAuthenticationException;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -763,7 +748,8 @@ public class BackupManager {
|
||||
* @param presentation A ZK credential presentation that encodes the backupId and the receipt level of the requester
|
||||
* @return A function that can be used to verify a signature provided with the presentation
|
||||
*/
|
||||
private PresentationSignatureVerifier verifyPresentation(final BackupAuthCredentialPresentation presentation) {
|
||||
private PresentationSignatureVerifier verifyPresentation(final BackupAuthCredentialPresentation presentation)
|
||||
throws BackupFailedZkAuthenticationException {
|
||||
try {
|
||||
presentation.verify(clock.instant(), serverSecretParams);
|
||||
} catch (VerificationFailedException e) {
|
||||
@@ -771,10 +757,7 @@ public class BackupManager {
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
FAILURE_REASON_TAG_NAME, "presentation_verification")
|
||||
.increment();
|
||||
throw Status.UNAUTHENTICATED
|
||||
.withDescription("backup auth credential presentation verification failed")
|
||||
.withCause(e)
|
||||
.asRuntimeException();
|
||||
throw new BackupFailedZkAuthenticationException("backup auth credential presentation verification failed");
|
||||
}
|
||||
return (signature, publicKey) -> {
|
||||
if (!publicKey.verifySignature(presentation.serialize(), signature)) {
|
||||
@@ -782,9 +765,7 @@ public class BackupManager {
|
||||
SUCCESS_TAG_NAME, String.valueOf(false),
|
||||
FAILURE_REASON_TAG_NAME, "signature_validation")
|
||||
.increment();
|
||||
throw Status.UNAUTHENTICATED
|
||||
.withDescription("backup auth credential presentation signature verification failed")
|
||||
.asRuntimeException();
|
||||
throw new BackupFailedZkAuthenticationException("backup auth credential presentation signature verification failed");
|
||||
}
|
||||
return new Pair<>(presentation.getType(), presentation.getBackupLevel());
|
||||
};
|
||||
@@ -795,19 +776,18 @@ public class BackupManager {
|
||||
*
|
||||
* @param backupUser The backup user to check
|
||||
* @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}
|
||||
* @throws BackupPermissionException if the backupUser is not authorized to access {@code backupLevel}
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel) {
|
||||
static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel)
|
||||
throws BackupPermissionException {
|
||||
if (backupUser.backupLevel().compareTo(backupLevel) < 0) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME, Tags.of(
|
||||
UserAgentTagUtil.getPlatformTag(backupUser.userAgent()),
|
||||
Tag.of(FAILURE_REASON_TAG_NAME, "level")))
|
||||
.increment();
|
||||
|
||||
throw Status.PERMISSION_DENIED
|
||||
.withDescription("credential does not support the requested operation")
|
||||
.asRuntimeException();
|
||||
throw new BackupPermissionException("credential does not support the requested operation");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -816,19 +796,17 @@ public class BackupManager {
|
||||
*
|
||||
* @param backupUser The backup user to check
|
||||
* @param credentialType The credential type to require
|
||||
* @throws {@link Status#UNAUTHENTICATED} error if the backup user is not authenticated with the given
|
||||
* @throws BackupWrongCredentialTypeException error if the backup user is not authenticated with the given
|
||||
* {@code credentialType}
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static void checkBackupCredentialType(final AuthenticatedBackupUser backupUser, final BackupCredentialType credentialType) {
|
||||
static void checkBackupCredentialType(final AuthenticatedBackupUser backupUser, final BackupCredentialType credentialType) throws BackupWrongCredentialTypeException {
|
||||
if (backupUser.credentialType() != credentialType) {
|
||||
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME,
|
||||
FAILURE_REASON_TAG_NAME, "credential_type")
|
||||
.increment();
|
||||
|
||||
throw Status.UNAUTHENTICATED
|
||||
.withDescription("wrong credential type for the requested operation")
|
||||
.asRuntimeException();
|
||||
throw new BackupWrongCredentialTypeException("wrong credential type for the requested operation");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
public class BackupMissingIdCommitmentException extends BackupException {
|
||||
public BackupMissingIdCommitmentException() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
public class BackupNotFoundException extends BackupException {
|
||||
|
||||
public BackupNotFoundException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
public class BackupPermissionException extends BackupException {
|
||||
public BackupPermissionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
public class BackupPublicKeyConflictException extends BackupException {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
public class BackupWrongCredentialTypeException extends BackupException {
|
||||
public BackupWrongCredentialTypeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import io.grpc.Status;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
@@ -22,18 +22,12 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
import io.micrometer.core.instrument.DistributionSummary;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
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;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import org.whispersystems.textsecuregcm.util.ExceptionUtils;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
@@ -133,7 +127,7 @@ public class BackupsDb {
|
||||
* @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}
|
||||
* does not match, the stage will be completed exceptionally with a {@link BackupPublicKeyConflictException}
|
||||
*/
|
||||
CompletableFuture<Void> setPublicKey(
|
||||
final byte[] authenticatedBackupId,
|
||||
@@ -152,7 +146,7 @@ public class BackupsDb {
|
||||
.build())
|
||||
.exceptionally(ExceptionUtils.marshal(ConditionalCheckFailedException.class, e ->
|
||||
// There was already a row for this backup-id and it contained a different publicKey
|
||||
new PublicKeyConflictException()))
|
||||
new BackupPublicKeyConflictException()))
|
||||
.thenRun(Util.NOOP);
|
||||
}
|
||||
|
||||
@@ -190,9 +184,7 @@ public class BackupsDb {
|
||||
private static String getDirName(final Map<String, AttributeValue> item, final String attr) {
|
||||
return AttributeValues.get(item, attr).map(AttributeValue::s).orElseThrow(() -> {
|
||||
logger.error("Backups with public keys should have directory names");
|
||||
return Status.INTERNAL
|
||||
.withDescription("Backups with public keys must have directory names")
|
||||
.asRuntimeException();
|
||||
throw new UncheckedIOException(new IOException("Backups with public keys must have directory names"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,10 +200,7 @@ public class BackupsDb {
|
||||
return new ECPublicKey(publicKeyBytes);
|
||||
} catch (InvalidKeyException e) {
|
||||
logger.error("Invalid publicKey {}", HexFormat.of().formatHex(publicKeyBytes), e);
|
||||
throw Status.INTERNAL
|
||||
.withCause(e)
|
||||
.withDescription("Could not deserialize stored public key")
|
||||
.asRuntimeException();
|
||||
throw new UncheckedIOException(new IOException("Could not deserialize stored public key"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +319,7 @@ public class BackupsDb {
|
||||
* @param backupUser an already authorized backup user
|
||||
* @return A {@link BackupDescription} containing the cdn of the message backup and the total number of media space
|
||||
* bytes used by the backup user.
|
||||
* @throws BackupNotFoundException If the provided backupUser's backup-id does not exist
|
||||
*/
|
||||
CompletableFuture<BackupDescription> describeBackup(final AuthenticatedBackupUser backupUser) {
|
||||
return dynamoClient.getItem(GetItemRequest.builder()
|
||||
@@ -341,7 +331,7 @@ public class BackupsDb {
|
||||
.build())
|
||||
.thenApply(response -> {
|
||||
if (!response.hasItem()) {
|
||||
throw Status.NOT_FOUND.withDescription("Backup ID not found").asRuntimeException();
|
||||
throw ExceptionUtils.wrap(new BackupNotFoundException("Backup ID not found"));
|
||||
}
|
||||
// If the client hasn't already uploaded a backup, return the cdn we would return if they did create one
|
||||
final int cdn = AttributeValues.getInt(response.item(), ATTR_CDN, BACKUP_CDN);
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class PublicKeyConflictException extends IOException {
|
||||
}
|
||||
@@ -67,8 +67,15 @@ import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.RedemptionRange;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupBadReceiptException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupFailedZkAuthenticationException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupInvalidArgumentException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupManager;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupMissingIdCommitmentException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupNotFoundException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupPermissionException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupUploadDescriptor;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupWrongCredentialTypeException;
|
||||
import org.whispersystems.textsecuregcm.backup.CopyParameters;
|
||||
import org.whispersystems.textsecuregcm.backup.CopyResult;
|
||||
import org.whispersystems.textsecuregcm.backup.MediaEncryptionParameters;
|
||||
@@ -150,7 +157,8 @@ public class ArchiveController {
|
||||
@ManagedAsync
|
||||
public void setBackupId(
|
||||
@Auth final AuthenticatedDevice authenticatedDevice,
|
||||
@Valid @NotNull final SetBackupIdRequest setBackupIdRequest) throws RateLimitExceededException {
|
||||
@Valid @NotNull final SetBackupIdRequest setBackupIdRequest)
|
||||
throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
|
||||
|
||||
final Account account = accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier())
|
||||
.orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
|
||||
@@ -230,14 +238,16 @@ public class ArchiveController {
|
||||
@ApiResponse(responseCode = "409", description = "The target account does not have a backup-id commitment")
|
||||
@ApiResponse(responseCode = "429", description = "Rate limited.")
|
||||
@ManagedAsync
|
||||
public void redeemReceipt(
|
||||
public Response redeemReceipt(
|
||||
@Auth final AuthenticatedDevice authenticatedDevice,
|
||||
@Valid @NotNull final RedeemBackupReceiptRequest redeemBackupReceiptRequest) {
|
||||
@Valid @NotNull final RedeemBackupReceiptRequest redeemBackupReceiptRequest)
|
||||
throws BackupInvalidArgumentException, BackupMissingIdCommitmentException, BackupBadReceiptException {
|
||||
|
||||
final Account account = accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier())
|
||||
.orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
|
||||
.orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
|
||||
|
||||
backupAuthManager.redeemReceipt(account, redeemBackupReceiptRequest.receiptCredentialPresentation());
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
public record BackupAuthCredentialsResponse(
|
||||
@@ -300,7 +310,7 @@ public class ArchiveController {
|
||||
@Auth AuthenticatedDevice authenticatedDevice,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||
@NotNull @QueryParam("redemptionStartSeconds") Long startSeconds,
|
||||
@NotNull @QueryParam("redemptionEndSeconds") Long endSeconds) {
|
||||
@NotNull @QueryParam("redemptionEndSeconds") Long endSeconds) throws BackupNotFoundException {
|
||||
|
||||
final Map<BackupCredentialType, List<BackupAuthCredentialsResponse.BackupAuthCredential>> credentialsByType =
|
||||
new HashMap<>();
|
||||
@@ -391,6 +401,7 @@ public class ArchiveController {
|
||||
summary = "Get CDN read credentials",
|
||||
description = "Retrieve credentials used to read objects stored on the backup cdn")
|
||||
@ApiResponse(responseCode = "200", content = @Content(schema = @Schema(implementation = ReadAuthResponse.class)))
|
||||
@ApiResponse(responseCode = "400", description = "Bad arguments. The request may have been made on an authenticated channel, or an invalid cdn number was provided")
|
||||
@ApiResponse(responseCode = "429", description = "Rate limited.")
|
||||
@ApiResponseZkAuth
|
||||
@ManagedAsync
|
||||
@@ -406,7 +417,8 @@ public class ArchiveController {
|
||||
@NotNull
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature,
|
||||
|
||||
@NotNull @Parameter(description = "The number of the CDN to get credentials for") @QueryParam("cdn") final Integer cdn) {
|
||||
@NotNull @Parameter(description = "The number of the CDN to get credentials for") @QueryParam("cdn") final Integer cdn)
|
||||
throws BackupFailedZkAuthenticationException, BackupInvalidArgumentException, BackupPermissionException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
@@ -436,7 +448,8 @@ public class ArchiveController {
|
||||
|
||||
@Parameter(description = BackupAuthCredentialPresentationSignature.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||
@NotNull
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature) {
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature)
|
||||
throws BackupFailedZkAuthenticationException, BackupWrongCredentialTypeException, BackupPermissionException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
@@ -487,7 +500,8 @@ public class ArchiveController {
|
||||
|
||||
@Parameter(description = BackupAuthCredentialPresentationSignature.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||
@NotNull
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature) {
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature)
|
||||
throws BackupFailedZkAuthenticationException, BackupNotFoundException, BackupPermissionException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
@@ -536,7 +550,8 @@ public class ArchiveController {
|
||||
@NotNull
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature,
|
||||
|
||||
@Valid @NotNull SetPublicKeyRequest setPublicKeyRequest) {
|
||||
@Valid @NotNull SetPublicKeyRequest setPublicKeyRequest)
|
||||
throws BackupFailedZkAuthenticationException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
@@ -578,7 +593,8 @@ public class ArchiveController {
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature,
|
||||
|
||||
@Parameter(description = "The size of the message backup to upload in bytes")
|
||||
@QueryParam("uploadLength") final Optional<Long> uploadLength) {
|
||||
@QueryParam("uploadLength") final Optional<Long> uploadLength)
|
||||
throws BackupFailedZkAuthenticationException, BackupWrongCredentialTypeException, BackupPermissionException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
@@ -630,7 +646,7 @@ public class ArchiveController {
|
||||
@Parameter(description = BackupAuthCredentialPresentationSignature.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||
@NotNull
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature)
|
||||
throws RateLimitExceededException {
|
||||
throws RateLimitExceededException, BackupFailedZkAuthenticationException, BackupWrongCredentialTypeException, BackupPermissionException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
@@ -723,15 +739,17 @@ public class ArchiveController {
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature,
|
||||
|
||||
@NotNull
|
||||
@Valid final ArchiveController.CopyMediaRequest copyMediaRequest) {
|
||||
@Valid final ArchiveController.CopyMediaRequest copyMediaRequest)
|
||||
throws BackupFailedZkAuthenticationException, BackupWrongCredentialTypeException, BackupPermissionException, BackupInvalidArgumentException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
|
||||
final AuthenticatedBackupUser backupUser =
|
||||
backupManager.authenticateBackupUser(presentation.presentation, signature.signature, userAgent);
|
||||
final CopyResult copyResult =
|
||||
backupManager.copyToBackup(backupUser, List.of(copyMediaRequest.toCopyParameters())).next()
|
||||
final BackupManager.CopyQuota copyQuota =
|
||||
backupManager.getCopyQuota(backupUser, List.of(copyMediaRequest.toCopyParameters()));
|
||||
final CopyResult copyResult = backupManager.copyToBackup(copyQuota).next()
|
||||
.blockOptional()
|
||||
.orElseThrow(() -> new IllegalStateException("Non empty copy request must return result"));
|
||||
backupMetrics.updateCopyCounter(copyResult, UserAgentTagUtil.getPlatformTag(userAgent));
|
||||
@@ -824,7 +842,8 @@ public class ArchiveController {
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature,
|
||||
|
||||
@NotNull
|
||||
@Valid final ArchiveController.CopyMediaBatchRequest copyMediaRequest) {
|
||||
@Valid final ArchiveController.CopyMediaBatchRequest copyMediaRequest)
|
||||
throws BackupFailedZkAuthenticationException, BackupWrongCredentialTypeException, BackupPermissionException, BackupInvalidArgumentException {
|
||||
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
@@ -832,7 +851,8 @@ public class ArchiveController {
|
||||
final Stream<CopyParameters> copyParams = copyMediaRequest.items().stream().map(CopyMediaRequest::toCopyParameters);
|
||||
final AuthenticatedBackupUser backupUser =
|
||||
backupManager.authenticateBackupUser(presentation.presentation, signature.signature, userAgent);
|
||||
final List<CopyMediaBatchResponse.Entry> copyResults = backupManager.copyToBackup(backupUser, copyParams.toList())
|
||||
final BackupManager.CopyQuota copyQuota = backupManager.getCopyQuota(backupUser, copyParams.toList());
|
||||
final List<CopyMediaBatchResponse.Entry> copyResults = backupManager.copyToBackup(copyQuota)
|
||||
.doOnNext(result -> backupMetrics.updateCopyCounter(result, UserAgentTagUtil.getPlatformTag(userAgent)))
|
||||
.map(CopyMediaBatchResponse.Entry::fromCopyResult)
|
||||
.collectList().block();
|
||||
@@ -861,7 +881,8 @@ public class ArchiveController {
|
||||
|
||||
@Parameter(description = BackupAuthCredentialPresentationSignature.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||
@NotNull
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature) {
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature)
|
||||
throws BackupFailedZkAuthenticationException, BackupPermissionException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
@@ -934,7 +955,8 @@ public class ArchiveController {
|
||||
@QueryParam("cursor") final Optional<String> cursor,
|
||||
|
||||
@Parameter(description = "The number of entries to return per call")
|
||||
@QueryParam("limit") final Optional<@Min(1) @Max(10_000) Integer> limit) {
|
||||
@QueryParam("limit") final Optional<@Min(1) @Max(10_000) Integer> limit)
|
||||
throws BackupPermissionException, BackupFailedZkAuthenticationException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
@@ -987,7 +1009,8 @@ public class ArchiveController {
|
||||
@NotNull
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature,
|
||||
|
||||
@Valid @NotNull DeleteMedia deleteMedia) {
|
||||
@Valid @NotNull DeleteMedia deleteMedia)
|
||||
throws BackupFailedZkAuthenticationException, BackupWrongCredentialTypeException, BackupPermissionException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
@@ -1020,7 +1043,8 @@ public class ArchiveController {
|
||||
|
||||
@Parameter(description = BackupAuthCredentialPresentationSignature.DESCRIPTION, schema = @Schema(implementation = String.class))
|
||||
@NotNull
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature) {
|
||||
@HeaderParam(X_SIGNAL_ZK_AUTH_SIGNATURE) final BackupAuthCredentialPresentationSignature signature)
|
||||
throws BackupPermissionException, BackupFailedZkAuthenticationException {
|
||||
if (account.isPresent()) {
|
||||
throw new BadRequestException("must not use authenticated connection for anonymous operations");
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import io.grpc.Status;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Flow;
|
||||
import org.signal.chat.backup.CopyMediaRequest;
|
||||
import org.signal.chat.backup.CopyMediaResponse;
|
||||
import org.signal.chat.backup.DeleteAllRequest;
|
||||
import org.signal.chat.backup.DeleteAllResponse;
|
||||
import org.signal.chat.backup.DeleteMediaItem;
|
||||
import org.signal.chat.backup.DeleteMediaRequest;
|
||||
import org.signal.chat.backup.DeleteMediaResponse;
|
||||
import org.signal.chat.backup.GetBackupInfoRequest;
|
||||
@@ -31,14 +30,21 @@ import org.signal.chat.backup.SetPublicKeyRequest;
|
||||
import org.signal.chat.backup.SetPublicKeyResponse;
|
||||
import org.signal.chat.backup.SignedPresentation;
|
||||
import org.signal.chat.backup.SimpleBackupsAnonymousGrpc;
|
||||
import org.signal.chat.errors.FailedPrecondition;
|
||||
import org.signal.chat.errors.FailedZkAuthentication;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
||||
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupFailedZkAuthenticationException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupInvalidArgumentException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupManager;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupNotFoundException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupPermissionException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupUploadDescriptor;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupWrongCredentialTypeException;
|
||||
import org.whispersystems.textsecuregcm.backup.CopyParameters;
|
||||
import org.whispersystems.textsecuregcm.backup.MediaEncryptionParameters;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
@@ -59,45 +65,75 @@ public class BackupsAnonymousGrpcService extends SimpleBackupsAnonymousGrpc.Back
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetCdnCredentialsResponse getCdnCredentials(final GetCdnCredentialsRequest request) {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
return GetCdnCredentialsResponse.newBuilder()
|
||||
.putAllHeaders(backupManager.generateReadAuth(backupUser, request.getCdn()))
|
||||
.build();
|
||||
public GetCdnCredentialsResponse getCdnCredentials(final GetCdnCredentialsRequest request)
|
||||
throws BackupInvalidArgumentException, BackupPermissionException {
|
||||
try {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
return GetCdnCredentialsResponse.newBuilder()
|
||||
.setCdnCredentials(GetCdnCredentialsResponse.CdnCredentials.newBuilder()
|
||||
.putAllHeaders(backupManager.generateReadAuth(backupUser, request.getCdn())))
|
||||
.build();
|
||||
} catch (BackupFailedZkAuthenticationException e) {
|
||||
return GetCdnCredentialsResponse.newBuilder()
|
||||
.setFailedAuthentication(FailedZkAuthentication.newBuilder().setDescription(e.getMessage()).build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetSvrBCredentialsResponse getSvrBCredentials(final GetSvrBCredentialsRequest request) {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
final ExternalServiceCredentials credentials = backupManager.generateSvrbAuth(backupUser);
|
||||
return GetSvrBCredentialsResponse.newBuilder()
|
||||
.setUsername(credentials.username())
|
||||
.setPassword(credentials.password())
|
||||
.build();
|
||||
public GetSvrBCredentialsResponse getSvrBCredentials(final GetSvrBCredentialsRequest request)
|
||||
throws BackupWrongCredentialTypeException, BackupPermissionException {
|
||||
try {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
final ExternalServiceCredentials credentials = backupManager.generateSvrbAuth(backupUser);
|
||||
return GetSvrBCredentialsResponse.newBuilder()
|
||||
.setSvrbCredentials(GetSvrBCredentialsResponse.SvrBCredentials.newBuilder()
|
||||
.setUsername(credentials.username())
|
||||
.setPassword(credentials.password()))
|
||||
.build();
|
||||
} catch (BackupFailedZkAuthenticationException e) {
|
||||
return GetSvrBCredentialsResponse.newBuilder()
|
||||
.setFailedAuthentication(FailedZkAuthentication.newBuilder().setDescription(e.getMessage()).build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetBackupInfoResponse getBackupInfo(final GetBackupInfoRequest request) {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
final BackupManager.BackupInfo info = backupManager.backupInfo(backupUser);
|
||||
return GetBackupInfoResponse.newBuilder()
|
||||
.setBackupName(info.messageBackupKey())
|
||||
.setCdn(info.cdn())
|
||||
.setBackupDir(info.backupSubdir())
|
||||
.setMediaDir(info.mediaSubdir())
|
||||
.setUsedSpace(info.mediaUsedSpace().orElse(0L))
|
||||
.build();
|
||||
public GetBackupInfoResponse getBackupInfo(final GetBackupInfoRequest request)
|
||||
throws BackupNotFoundException, BackupPermissionException {
|
||||
try {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
final BackupManager.BackupInfo info = backupManager.backupInfo(backupUser);
|
||||
return GetBackupInfoResponse.newBuilder().setBackupInfo(GetBackupInfoResponse.BackupInfo.newBuilder()
|
||||
.setBackupName(info.messageBackupKey())
|
||||
.setCdn(info.cdn())
|
||||
.setBackupDir(info.backupSubdir())
|
||||
.setMediaDir(info.mediaSubdir())
|
||||
.setUsedSpace(info.mediaUsedSpace().orElse(0L)))
|
||||
.build();
|
||||
} catch (BackupFailedZkAuthenticationException e) {
|
||||
return GetBackupInfoResponse.newBuilder()
|
||||
.setFailedAuthentication(FailedZkAuthentication.newBuilder().setDescription(e.getMessage()).build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RefreshResponse refresh(final RefreshRequest request) {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
backupManager.ttlRefresh(backupUser);
|
||||
return RefreshResponse.getDefaultInstance();
|
||||
public RefreshResponse refresh(final RefreshRequest request) throws BackupPermissionException {
|
||||
try {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
backupManager.ttlRefresh(backupUser);
|
||||
return RefreshResponse.getDefaultInstance();
|
||||
} catch (BackupFailedZkAuthenticationException e) {
|
||||
return RefreshResponse.newBuilder()
|
||||
.setFailedAuthentication(FailedZkAuthentication.newBuilder().setDescription(e.getMessage()).build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SetPublicKeyResponse setPublicKey(final SetPublicKeyRequest request) {
|
||||
public SetPublicKeyResponse setPublicKey(final SetPublicKeyRequest request)
|
||||
throws BackupFailedZkAuthenticationException {
|
||||
final ECPublicKey publicKey = deserialize(ECPublicKey::new, request.getPublicKey().toByteArray());
|
||||
final BackupAuthCredentialPresentation presentation = deserialize(
|
||||
BackupAuthCredentialPresentation::new,
|
||||
@@ -110,45 +146,67 @@ public class BackupsAnonymousGrpcService extends SimpleBackupsAnonymousGrpc.Back
|
||||
|
||||
|
||||
@Override
|
||||
public GetUploadFormResponse getUploadForm(final GetUploadFormRequest request) throws RateLimitExceededException {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
final BackupUploadDescriptor uploadDescriptor = switch (request.getUploadTypeCase()) {
|
||||
public GetUploadFormResponse getUploadForm(final GetUploadFormRequest request)
|
||||
throws RateLimitExceededException, BackupWrongCredentialTypeException, BackupPermissionException {
|
||||
final AuthenticatedBackupUser backupUser;
|
||||
try {
|
||||
backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
} catch (BackupFailedZkAuthenticationException e) {
|
||||
return GetUploadFormResponse.newBuilder()
|
||||
.setFailedAuthentication(FailedZkAuthentication.newBuilder().setDescription(e.getMessage()).build())
|
||||
.build();
|
||||
}
|
||||
final GetUploadFormResponse.Builder builder = GetUploadFormResponse.newBuilder();
|
||||
switch (request.getUploadTypeCase()) {
|
||||
case MESSAGES -> {
|
||||
final long uploadLength = request.getMessages().getUploadLength();
|
||||
final boolean oversize = uploadLength > BackupManager.MAX_MESSAGE_BACKUP_OBJECT_SIZE;
|
||||
backupMetrics.updateMessageBackupSizeDistribution(backupUser, oversize, Optional.of(uploadLength));
|
||||
if (oversize) {
|
||||
throw Status.FAILED_PRECONDITION
|
||||
.withDescription("Exceeds max upload length")
|
||||
.asRuntimeException();
|
||||
builder.setExceedsMaxUploadLength(FailedPrecondition.getDefaultInstance());
|
||||
} else {
|
||||
final BackupUploadDescriptor uploadDescriptor = backupManager.createMessageBackupUploadDescriptor(backupUser);
|
||||
builder.setUploadForm(builder.getUploadFormBuilder()
|
||||
.setCdn(uploadDescriptor.cdn())
|
||||
.setKey(uploadDescriptor.key())
|
||||
.setSignedUploadLocation(uploadDescriptor.signedUploadLocation())
|
||||
.putAllHeaders(uploadDescriptor.headers())).build();
|
||||
}
|
||||
|
||||
yield backupManager.createMessageBackupUploadDescriptor(backupUser);
|
||||
}
|
||||
case MEDIA -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser);
|
||||
case UPLOADTYPE_NOT_SET -> throw Status.INVALID_ARGUMENT
|
||||
.withDescription("Must set upload_type")
|
||||
.asRuntimeException();
|
||||
};
|
||||
return GetUploadFormResponse.newBuilder()
|
||||
.setCdn(uploadDescriptor.cdn())
|
||||
.setKey(uploadDescriptor.key())
|
||||
.setSignedUploadLocation(uploadDescriptor.signedUploadLocation())
|
||||
.putAllHeaders(uploadDescriptor.headers())
|
||||
.build();
|
||||
case MEDIA -> {
|
||||
final BackupUploadDescriptor uploadDescriptor = backupManager.createTemporaryAttachmentUploadDescriptor(
|
||||
backupUser);
|
||||
builder.setUploadForm(builder.getUploadFormBuilder()
|
||||
.setCdn(uploadDescriptor.cdn())
|
||||
.setKey(uploadDescriptor.key())
|
||||
.setSignedUploadLocation(uploadDescriptor.signedUploadLocation())
|
||||
.putAllHeaders(uploadDescriptor.headers())).build();
|
||||
}
|
||||
case UPLOADTYPE_NOT_SET -> throw GrpcExceptions.fieldViolation("upload_type", "Must set upload_type");
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flow.Publisher<CopyMediaResponse> copyMedia(final CopyMediaRequest request) {
|
||||
final Flux<CopyMediaResponse> flux = Mono
|
||||
.fromFuture(() -> authenticateBackupUserAsync(request.getSignedPresentation()))
|
||||
.flatMapMany(backupUser -> backupManager.copyToBackup(backupUser,
|
||||
request.getItemsList().stream().map(item -> new CopyParameters(
|
||||
item.getSourceAttachmentCdn(), item.getSourceKey(),
|
||||
// uint32 in proto, make sure it fits in a signed int
|
||||
fromUnsignedExact(item.getObjectLength()),
|
||||
new MediaEncryptionParameters(item.getEncryptionKey().toByteArray(), item.getHmacKey().toByteArray()),
|
||||
item.getMediaId().toByteArray())).toList()))
|
||||
public Flow.Publisher<CopyMediaResponse> copyMedia(final CopyMediaRequest request)
|
||||
throws BackupWrongCredentialTypeException, BackupPermissionException, BackupInvalidArgumentException {
|
||||
final BackupManager.CopyQuota copyQuota;
|
||||
try {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
copyQuota = backupManager.getCopyQuota(backupUser,
|
||||
request.getItemsList().stream().map(item -> new CopyParameters(
|
||||
item.getSourceAttachmentCdn(), item.getSourceKey(),
|
||||
// uint32 in proto, make sure it fits in a signed int
|
||||
fromUnsignedExact(item.getObjectLength()),
|
||||
new MediaEncryptionParameters(item.getEncryptionKey().toByteArray(), item.getHmacKey().toByteArray()),
|
||||
item.getMediaId().toByteArray())).toList());
|
||||
} catch (BackupFailedZkAuthenticationException e) {
|
||||
return JdkFlowAdapter.publisherToFlowPublisher(Mono.just(CopyMediaResponse
|
||||
.newBuilder()
|
||||
.setFailedAuthentication(FailedZkAuthentication.newBuilder().setDescription(e.getMessage()))
|
||||
.build()));
|
||||
}
|
||||
return JdkFlowAdapter.publisherToFlowPublisher(backupManager.copyToBackup(copyQuota)
|
||||
.doOnNext(result -> backupMetrics.updateCopyCounter(
|
||||
result,
|
||||
UserAgentTagUtil.getPlatformTag(RequestAttributesUtil.getUserAgent().orElse(null))))
|
||||
@@ -167,79 +225,107 @@ public class BackupsAnonymousGrpcService extends SimpleBackupsAnonymousGrpc.Back
|
||||
.setSourceNotFound(CopyMediaResponse.SourceNotFound.getDefaultInstance());
|
||||
};
|
||||
return builder.build();
|
||||
});
|
||||
return JdkFlowAdapter.publisherToFlowPublisher(flux);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListMediaResponse listMedia(final ListMediaRequest request) {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
final BackupManager.ListMediaResult listResult = backupManager.list(
|
||||
backupUser,
|
||||
request.hasCursor() ? Optional.of(request.getCursor()) : Optional.empty(),
|
||||
request.getLimit());
|
||||
|
||||
final ListMediaResponse.Builder builder = ListMediaResponse.newBuilder();
|
||||
for (BackupManager.StorageDescriptorWithLength sd : listResult.media()) {
|
||||
builder.addPage(ListMediaResponse.ListEntry.newBuilder()
|
||||
.setMediaId(ByteString.copyFrom(sd.key()))
|
||||
.setCdn(sd.cdn())
|
||||
.setLength(sd.length())
|
||||
.build());
|
||||
public ListMediaResponse listMedia(final ListMediaRequest request) throws BackupPermissionException {
|
||||
try {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
final BackupManager.ListMediaResult listResult = backupManager.list(
|
||||
backupUser,
|
||||
request.hasCursor() ? Optional.of(request.getCursor()) : Optional.empty(),
|
||||
request.getLimit());
|
||||
final ListMediaResponse.ListResult.Builder builder = ListMediaResponse.ListResult.newBuilder();
|
||||
for (BackupManager.StorageDescriptorWithLength sd : listResult.media()) {
|
||||
builder.addPage(ListMediaResponse.ListEntry.newBuilder()
|
||||
.setMediaId(ByteString.copyFrom(sd.key()))
|
||||
.setCdn(sd.cdn())
|
||||
.setLength(sd.length())
|
||||
.build());
|
||||
}
|
||||
builder
|
||||
.setBackupDir(backupUser.backupDir())
|
||||
.setMediaDir(backupUser.mediaDir());
|
||||
listResult.cursor().ifPresent(builder::setCursor);
|
||||
return ListMediaResponse.newBuilder().setListResult(builder).build();
|
||||
} catch (BackupFailedZkAuthenticationException e) {
|
||||
return ListMediaResponse.newBuilder()
|
||||
.setFailedAuthentication(FailedZkAuthentication.newBuilder().setDescription(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
builder
|
||||
.setBackupDir(backupUser.backupDir())
|
||||
.setMediaDir(backupUser.mediaDir());
|
||||
listResult.cursor().ifPresent(builder::setCursor);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeleteAllResponse deleteAll(final DeleteAllRequest request) {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
backupManager.deleteEntireBackup(backupUser);
|
||||
return DeleteAllResponse.getDefaultInstance();
|
||||
public DeleteAllResponse deleteAll(final DeleteAllRequest request) throws BackupPermissionException {
|
||||
try {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
backupManager.deleteEntireBackup(backupUser);
|
||||
return DeleteAllResponse.getDefaultInstance();
|
||||
} catch (BackupFailedZkAuthenticationException e) {
|
||||
return DeleteAllResponse.newBuilder()
|
||||
.setFailedAuthentication(FailedZkAuthentication.newBuilder().setDescription(e.getMessage()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flow.Publisher<DeleteMediaResponse> deleteMedia(final DeleteMediaRequest request) {
|
||||
return JdkFlowAdapter.publisherToFlowPublisher(Mono
|
||||
.fromFuture(() -> authenticateBackupUserAsync(request.getSignedPresentation()))
|
||||
.flatMapMany(backupUser -> backupManager.deleteMedia(backupUser, request
|
||||
.getItemsList()
|
||||
.stream()
|
||||
.map(item -> new BackupManager.StorageDescriptor(item.getCdn(), item.getMediaId().toByteArray()))
|
||||
.toList()))
|
||||
public Flow.Publisher<DeleteMediaResponse> deleteMedia(final DeleteMediaRequest request)
|
||||
throws BackupWrongCredentialTypeException, BackupPermissionException {
|
||||
final Flux<BackupManager.StorageDescriptor> deleteItems;
|
||||
try {
|
||||
final AuthenticatedBackupUser backupUser = authenticateBackupUser(request.getSignedPresentation());
|
||||
deleteItems = backupManager.deleteMedia(backupUser, request
|
||||
.getItemsList()
|
||||
.stream()
|
||||
.map(item -> new BackupManager.StorageDescriptor(item.getCdn(), item.getMediaId().toByteArray()))
|
||||
.toList());
|
||||
} catch (BackupFailedZkAuthenticationException e) {
|
||||
return JdkFlowAdapter.publisherToFlowPublisher(Mono.just(DeleteMediaResponse
|
||||
.newBuilder()
|
||||
.setFailedAuthentication(FailedZkAuthentication.newBuilder().setDescription(e.getMessage()))
|
||||
.build()));
|
||||
}
|
||||
return JdkFlowAdapter.publisherToFlowPublisher(deleteItems
|
||||
.map(storageDescriptor -> DeleteMediaResponse.newBuilder()
|
||||
.setMediaId(ByteString.copyFrom(storageDescriptor.key()))
|
||||
.setCdn(storageDescriptor.cdn()).build()));
|
||||
.setDeletedItem(DeleteMediaItem.newBuilder()
|
||||
.setMediaId(ByteString.copyFrom(storageDescriptor.key()))
|
||||
.setCdn(storageDescriptor.cdn()))
|
||||
.build()));
|
||||
}
|
||||
|
||||
private CompletableFuture<AuthenticatedBackupUser> authenticateBackupUserAsync(final SignedPresentation signedPresentation) {
|
||||
@Override
|
||||
public Throwable mapException(final Throwable throwable) {
|
||||
return switch (throwable) {
|
||||
case BackupInvalidArgumentException e -> GrpcExceptions.invalidArguments(e.getMessage());
|
||||
case BackupPermissionException e -> GrpcExceptions.badAuthentication(e.getMessage());
|
||||
case BackupWrongCredentialTypeException e -> GrpcExceptions.badAuthentication(e.getMessage());
|
||||
default -> throwable;
|
||||
};
|
||||
}
|
||||
|
||||
private AuthenticatedBackupUser authenticateBackupUser(final SignedPresentation signedPresentation)
|
||||
throws BackupFailedZkAuthenticationException {
|
||||
if (signedPresentation == null) {
|
||||
throw Status.UNAUTHENTICATED.asRuntimeException();
|
||||
throw GrpcExceptions.badAuthentication("Missing required signedPresentation");
|
||||
}
|
||||
try {
|
||||
return backupManager.authenticateBackupUserAsync(
|
||||
return backupManager.authenticateBackupUser(
|
||||
new BackupAuthCredentialPresentation(signedPresentation.getPresentation().toByteArray()),
|
||||
signedPresentation.getPresentationSignature().toByteArray(),
|
||||
RequestAttributesUtil.getUserAgent().orElse(null));
|
||||
} catch (InvalidInputException e) {
|
||||
throw Status.UNAUTHENTICATED.withDescription("Could not deserialize presentation").asRuntimeException();
|
||||
throw GrpcExceptions.badAuthentication("Could not deserialize presentation");
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticatedBackupUser authenticateBackupUser(final SignedPresentation signedPresentation) {
|
||||
return authenticateBackupUserAsync(signedPresentation).join();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an int from a proto uint32 to a signed positive integer, throwing if the value exceeds
|
||||
* {@link Integer#MAX_VALUE}. To convert to a long, see {@link Integer#toUnsignedLong(int)}
|
||||
*/
|
||||
private static int fromUnsignedExact(final int i) {
|
||||
if (i < 0) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("Invalid size").asRuntimeException();
|
||||
throw GrpcExceptions.invalidArguments("integer length too large");
|
||||
}
|
||||
return i;
|
||||
}
|
||||
@@ -253,7 +339,7 @@ public class BackupsAnonymousGrpcService extends SimpleBackupsAnonymousGrpc.Back
|
||||
try {
|
||||
return deserializer.deserialize(bytes);
|
||||
} catch (InvalidInputException | InvalidKeyException e) {
|
||||
throw Status.INVALID_ARGUMENT.withDescription("Invalid serialization").asRuntimeException();
|
||||
throw GrpcExceptions.invalidArguments("invalid serialization");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.Empty;
|
||||
import io.grpc.Status;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.time.Clock;
|
||||
@@ -20,6 +21,7 @@ import org.signal.chat.backup.SetBackupIdRequest;
|
||||
import org.signal.chat.backup.SetBackupIdResponse;
|
||||
import org.signal.chat.backup.SimpleBackupsGrpc;
|
||||
import org.signal.chat.common.ZkCredential;
|
||||
import org.signal.chat.errors.FailedPrecondition;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
|
||||
@@ -28,6 +30,12 @@ import org.whispersystems.textsecuregcm.auth.RedemptionRange;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupBadReceiptException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupInvalidArgumentException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupMissingIdCommitmentException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupNotFoundException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupPermissionException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupWrongCredentialTypeException;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.metrics.BackupMetrics;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
@@ -48,7 +56,8 @@ public class BackupsGrpcService extends SimpleBackupsGrpc.BackupsImplBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public SetBackupIdResponse setBackupId(SetBackupIdRequest request) throws RateLimitExceededException {
|
||||
public SetBackupIdResponse setBackupId(SetBackupIdRequest request)
|
||||
throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
|
||||
|
||||
final Optional<BackupAuthCredentialRequest> messagesCredentialRequest = deserializeWithEmptyPresenceCheck(
|
||||
BackupAuthCredentialRequest::new,
|
||||
@@ -67,13 +76,21 @@ public class BackupsGrpcService extends SimpleBackupsGrpc.BackupsImplBase {
|
||||
return SetBackupIdResponse.getDefaultInstance();
|
||||
}
|
||||
|
||||
public RedeemReceiptResponse redeemReceipt(RedeemReceiptRequest request) {
|
||||
public RedeemReceiptResponse redeemReceipt(RedeemReceiptRequest request) throws BackupInvalidArgumentException {
|
||||
final ReceiptCredentialPresentation receiptCredentialPresentation = deserialize(
|
||||
ReceiptCredentialPresentation::new,
|
||||
request.getPresentation().toByteArray());
|
||||
final Account account = authenticatedAccount();
|
||||
backupAuthManager.redeemReceipt(account, receiptCredentialPresentation);
|
||||
return RedeemReceiptResponse.getDefaultInstance();
|
||||
final RedeemReceiptResponse.Builder builder = RedeemReceiptResponse.newBuilder();
|
||||
try {
|
||||
backupAuthManager.redeemReceipt(account, receiptCredentialPresentation);
|
||||
builder.setSuccess(Empty.getDefaultInstance());
|
||||
} catch (BackupBadReceiptException e) {
|
||||
builder.setInvalidReceipt(FailedPrecondition.newBuilder().setDescription(e.getMessage()).build());
|
||||
} catch (BackupMissingIdCommitmentException e) {
|
||||
builder.setAccountMissingCommitment(FailedPrecondition.newBuilder().build());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,34 +105,51 @@ public class BackupsGrpcService extends SimpleBackupsGrpc.BackupsImplBase {
|
||||
throw Status.INVALID_ARGUMENT.withDescription(e.getMessage()).asRuntimeException();
|
||||
}
|
||||
final Account account = authenticatedAccount();
|
||||
final List<BackupAuthManager.Credential> messageCredentials =
|
||||
backupAuthManager.getBackupAuthCredentials(
|
||||
account,
|
||||
BackupCredentialType.MESSAGES,
|
||||
redemptionRange);
|
||||
backupMetrics.updateGetCredentialCounter(platformTag, BackupCredentialType.MESSAGES, messageCredentials.size());
|
||||
try {
|
||||
final List<BackupAuthManager.Credential> messageCredentials =
|
||||
backupAuthManager.getBackupAuthCredentials(
|
||||
account,
|
||||
BackupCredentialType.MESSAGES,
|
||||
redemptionRange);
|
||||
backupMetrics.updateGetCredentialCounter(platformTag, BackupCredentialType.MESSAGES, messageCredentials.size());
|
||||
|
||||
final List<BackupAuthManager.Credential> mediaCredentials =
|
||||
backupAuthManager.getBackupAuthCredentials(
|
||||
account,
|
||||
BackupCredentialType.MEDIA,
|
||||
redemptionRange);
|
||||
backupMetrics.updateGetCredentialCounter(platformTag, BackupCredentialType.MEDIA, mediaCredentials.size());
|
||||
final List<BackupAuthManager.Credential> mediaCredentials =
|
||||
backupAuthManager.getBackupAuthCredentials(
|
||||
account,
|
||||
BackupCredentialType.MEDIA,
|
||||
redemptionRange);
|
||||
backupMetrics.updateGetCredentialCounter(platformTag, BackupCredentialType.MEDIA, mediaCredentials.size());
|
||||
|
||||
return GetBackupAuthCredentialsResponse.newBuilder()
|
||||
.putAllMessageCredentials(messageCredentials.stream().collect(Collectors.toMap(
|
||||
c -> c.redemptionTime().getEpochSecond(),
|
||||
c -> ZkCredential.newBuilder()
|
||||
.setCredential(ByteString.copyFrom(c.credential().serialize()))
|
||||
.setRedemptionTime(c.redemptionTime().getEpochSecond())
|
||||
.build())))
|
||||
.putAllMediaCredentials(mediaCredentials.stream().collect(Collectors.toMap(
|
||||
c -> c.redemptionTime().getEpochSecond(),
|
||||
c -> ZkCredential.newBuilder()
|
||||
.setCredential(ByteString.copyFrom(c.credential().serialize()))
|
||||
.setRedemptionTime(c.redemptionTime().getEpochSecond())
|
||||
.build())))
|
||||
.build();
|
||||
return GetBackupAuthCredentialsResponse.newBuilder()
|
||||
.setCredentials(GetBackupAuthCredentialsResponse.Credentials.newBuilder()
|
||||
.putAllMessageCredentials(messageCredentials.stream().collect(Collectors.toMap(
|
||||
c -> c.redemptionTime().getEpochSecond(),
|
||||
c -> ZkCredential.newBuilder()
|
||||
.setCredential(ByteString.copyFrom(c.credential().serialize()))
|
||||
.setRedemptionTime(c.redemptionTime().getEpochSecond())
|
||||
.build())))
|
||||
.putAllMediaCredentials(mediaCredentials.stream().collect(Collectors.toMap(
|
||||
c -> c.redemptionTime().getEpochSecond(),
|
||||
c -> ZkCredential.newBuilder()
|
||||
.setCredential(ByteString.copyFrom(c.credential().serialize()))
|
||||
.setRedemptionTime(c.redemptionTime().getEpochSecond())
|
||||
.build())))
|
||||
.build())
|
||||
.build();
|
||||
} catch (BackupNotFoundException _) {
|
||||
// Return an empty response to indicate that the authenticated account had no associated blinded backup-id
|
||||
return GetBackupAuthCredentialsResponse.getDefaultInstance();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable mapException(final Throwable throwable) {
|
||||
return switch (throwable) {
|
||||
case BackupInvalidArgumentException e -> GrpcExceptions.invalidArguments(e.getMessage());
|
||||
case BackupPermissionException e -> GrpcExceptions.badAuthentication(e.getMessage());
|
||||
case BackupWrongCredentialTypeException e -> GrpcExceptions.badAuthentication(e.getMessage());
|
||||
default -> throwable;
|
||||
};
|
||||
}
|
||||
|
||||
private Account authenticatedAccount() {
|
||||
|
||||
@@ -79,6 +79,20 @@ public class GrpcExceptions {
|
||||
.build());
|
||||
}
|
||||
|
||||
/// The RPC argument violated a constraint that was annotated or documented in the service definition. It is always
|
||||
/// possible to check this constraint without communicating with the chat server. This always represents a client bug
|
||||
/// or out of date client.
|
||||
///
|
||||
/// @param message Additional context about the constraint violation
|
||||
/// @return A [StatusRuntimeException] encoding the error
|
||||
public static StatusRuntimeException invalidArguments(@Nullable final String message) {
|
||||
return StatusProto.toStatusRuntimeException(com.google.rpc.Status.newBuilder()
|
||||
.setCode(Status.Code.INVALID_ARGUMENT.value())
|
||||
.setMessage(messageOrDefault(message, Status.Code.INVALID_ARGUMENT))
|
||||
.addDetails(ERROR_INFO_CONSTRAINT_VIOLATED)
|
||||
.build());
|
||||
}
|
||||
|
||||
/// The RPC argument violated a constraint that was annotated or documented in the service definition. It is always
|
||||
/// possible to check this constraint without communicating with the chat server. This always represents a client bug
|
||||
/// or out of date client.
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.mappers;
|
||||
|
||||
import io.dropwizard.jersey.errors.ErrorMessage;
|
||||
import jakarta.ws.rs.WebApplicationException;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupBadReceiptException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupFailedZkAuthenticationException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupInvalidArgumentException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupMissingIdCommitmentException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupNotFoundException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupPermissionException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupWrongCredentialTypeException;
|
||||
|
||||
public class BackupExceptionMapper implements ExceptionMapper<BackupException> {
|
||||
|
||||
@Override
|
||||
public Response toResponse(final BackupException exception) {
|
||||
final Response.Status status = (switch (exception) {
|
||||
case BackupNotFoundException _ -> Response.Status.NOT_FOUND;
|
||||
case BackupInvalidArgumentException _, BackupBadReceiptException _ -> Response.Status.BAD_REQUEST;
|
||||
case BackupPermissionException _ -> Response.Status.FORBIDDEN;
|
||||
case BackupMissingIdCommitmentException _ -> Response.Status.CONFLICT;
|
||||
case BackupWrongCredentialTypeException _,
|
||||
BackupFailedZkAuthenticationException _ -> Response.Status.UNAUTHORIZED;
|
||||
default -> Response.Status.INTERNAL_SERVER_ERROR;
|
||||
});
|
||||
|
||||
final WebApplicationException wae =
|
||||
new WebApplicationException(exception.getMessage(), exception, Response.status(status).build());
|
||||
|
||||
return Response
|
||||
.fromResponse(wae.getResponse())
|
||||
.type(MediaType.APPLICATION_JSON_TYPE)
|
||||
.entity(new ErrorMessage(wae.getResponse().getStatus(), wae.getLocalizedMessage())).build();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,9 @@ package org.signal.chat.backup;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
import "org/signal/chat/common.proto";
|
||||
import "org/signal/chat/errors.proto";
|
||||
import "org/signal/chat/require.proto";
|
||||
import "org/signal/chat/tag.proto";
|
||||
|
||||
/**
|
||||
* Service for backup operations that require account authentication.
|
||||
@@ -19,16 +21,6 @@ import "org/signal/chat/require.proto";
|
||||
* Most actual backup operations operate on the backup-id and cannot be linked
|
||||
* to the caller's account, but setting up anonymous credentials and changing
|
||||
* backup tier requires account authentication.
|
||||
*
|
||||
* All rpcs on this service may return these errors. rpc specific errors
|
||||
* documented on the individual rpc.
|
||||
*
|
||||
* errors:
|
||||
* UNAUTHENTICATED Authentication failed or the account does not exist
|
||||
* INVALID_ARGUMENT The request did not meet a documented requirement
|
||||
* RESOURCE_EXHAUSTED Rate limit exceeded. A retry-after header containing an
|
||||
* ISO8601 duration string will be present in the response
|
||||
* trailers.
|
||||
*/
|
||||
service Backups {
|
||||
option (require.auth) = AUTH_ONLY_AUTHENTICATED;
|
||||
@@ -44,9 +36,7 @@ service Backups {
|
||||
* from a recoverable secret.
|
||||
*
|
||||
* At least one of the credential types must be set on the request.
|
||||
*
|
||||
* errors:
|
||||
* PERMISSION_DENIED: This account is not currently eligible for backups
|
||||
* Only the primary device can set a blinded backup-id.
|
||||
*/
|
||||
rpc SetBackupId(SetBackupIdRequest) returns (SetBackupIdResponse) {}
|
||||
|
||||
@@ -57,9 +47,6 @@ service Backups {
|
||||
* After successful redemption, subsequent requests to
|
||||
* GetBackupAuthCredentials will return credentials with the level on the
|
||||
* provided receipt until the expiration time on the receipt.
|
||||
*
|
||||
* errors:
|
||||
* ABORTED: The target account does not have a backup-id commitment
|
||||
*/
|
||||
rpc RedeemReceipt(RedeemReceiptRequest) returns (RedeemReceiptResponse) {}
|
||||
|
||||
@@ -79,9 +66,6 @@ service Backups {
|
||||
* Clients must validate the receipt level on the credential matches a known
|
||||
* receipt level before using it.
|
||||
*
|
||||
* errors:
|
||||
* NOT_FOUND: Could not find an existing blinded backup id associated with the
|
||||
* account.
|
||||
*/
|
||||
rpc GetBackupAuthCredentials(GetBackupAuthCredentialsRequest) returns (GetBackupAuthCredentialsResponse) {}
|
||||
}
|
||||
@@ -103,6 +87,7 @@ message SetBackupIdRequest {
|
||||
*/
|
||||
bytes media_backup_auth_credential_request = 2;
|
||||
}
|
||||
|
||||
message SetBackupIdResponse {}
|
||||
|
||||
|
||||
@@ -112,7 +97,25 @@ message RedeemReceiptRequest {
|
||||
*/
|
||||
bytes presentation = 1;
|
||||
}
|
||||
message RedeemReceiptResponse {}
|
||||
|
||||
message RedeemReceiptResponse {
|
||||
oneof outcome {
|
||||
/**
|
||||
* The receipt was successfully redeemed
|
||||
*/
|
||||
google.protobuf.Empty success = 1;
|
||||
|
||||
/**
|
||||
* The target account does not have a backup-id commitment
|
||||
*/
|
||||
errors.FailedPrecondition account_missing_commitment = 2 [(tag.reason) = "account_missing_commitment"];
|
||||
|
||||
/**
|
||||
* The provided receipt presentation was malformed or expired
|
||||
*/
|
||||
errors.FailedPrecondition invalid_receipt = 3 [(tag.reason) = "invalid_receipt"];
|
||||
}
|
||||
}
|
||||
|
||||
message GetBackupAuthCredentialsRequest {
|
||||
/**
|
||||
@@ -130,21 +133,27 @@ message GetBackupAuthCredentialsRequest {
|
||||
}
|
||||
|
||||
message GetBackupAuthCredentialsResponse {
|
||||
/**
|
||||
* The requested message backup ZkCredentials indexed by the start of their
|
||||
* validity period. The smallest key should be for the requested
|
||||
* redemption_start, the largest for the requested the requested
|
||||
* redemption_end.
|
||||
*/
|
||||
map<int64, common.ZkCredential> message_credentials = 1;
|
||||
message Credentials {
|
||||
/**
|
||||
* The requested message backup ZkCredentials indexed by the start of their
|
||||
* validity period. The smallest key should be for the requested
|
||||
* redemption_start, the largest for the requested redemption_end.
|
||||
*/
|
||||
map<int64, common.ZkCredential> message_credentials = 1;
|
||||
|
||||
/**
|
||||
* The requested media backup ZkCredentials indexed by the start of their
|
||||
* validity period. The smallest key should be for the requested
|
||||
* redemption_start, the largest for the requested redemption_end.
|
||||
*/
|
||||
map<int64, common.ZkCredential> media_credentials = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* The requested media backup ZkCredentials indexed by the start of their
|
||||
* validity period. The smallest key should be for the requested
|
||||
* redemption_start, the largest for the requested the requested
|
||||
* redemption_end.
|
||||
* The requested credentials. If absent, there was no existing blinded
|
||||
* backup id associated with the provided account.
|
||||
*/
|
||||
map<int64, common.ZkCredential> media_credentials = 2;
|
||||
Credentials credentials = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,21 +176,6 @@ message GetBackupAuthCredentialsResponse {
|
||||
* - a signature of that presentation using the private key of a key pair
|
||||
* previously set with SetPublicKey.
|
||||
*
|
||||
* All RPCs on this service may return these errors. RPC specific errors
|
||||
* documented on the individual RPC.
|
||||
*
|
||||
* errors:
|
||||
* UNAUTHENTICATED Either the presentation was missing, the credential was
|
||||
* expired, presentation verification failed, the signature
|
||||
* was incorrect, there was no committed public key for the
|
||||
* corresponding backup id, or the request was made on a
|
||||
* non-anonymous channel.
|
||||
* PERMISSION_DENIED The credential does not have permission to perform the
|
||||
* requested action.
|
||||
* RESOURCE_EXHAUSTED Rate limit exceeded. A retry-after header containing an
|
||||
* ISO8601 duration string will be present in the response
|
||||
* trailers.
|
||||
* INVALID_ARGUMENT The request did not meet a documented requirement
|
||||
*/
|
||||
service BackupsAnonymous {
|
||||
option (require.auth) = AUTH_ONLY_ANONYMOUS;
|
||||
@@ -205,9 +199,6 @@ service BackupsAnonymous {
|
||||
* Permanently set the public key of an ED25519 key-pair for the backup-id.
|
||||
* All requests (including this one!) must sign their BackupAuthCredential
|
||||
* presentations with the private key corresponding to the provided public key.
|
||||
*
|
||||
* Trying to set a public key when a different one is already set will return
|
||||
* an UNAUTHENTICATED error.
|
||||
*/
|
||||
rpc SetPublicKey(SetPublicKeyRequest) returns (SetPublicKeyResponse) {}
|
||||
|
||||
@@ -220,10 +211,6 @@ service BackupsAnonymous {
|
||||
|
||||
/**
|
||||
* Retrieve an upload form that can be used to perform a resumable upload
|
||||
*
|
||||
* Trying to request an upload form larger than the maximum supported upload
|
||||
* size will return a PRECONDITION_FAILED error. The maximum upload size is
|
||||
* subject to change.
|
||||
*/
|
||||
rpc GetUploadForm(GetUploadFormRequest) returns (GetUploadFormResponse) {}
|
||||
|
||||
@@ -285,76 +272,137 @@ message SetPublicKeyRequest {
|
||||
*/
|
||||
bytes public_key = 2;
|
||||
}
|
||||
message SetPublicKeyResponse {}
|
||||
message SetPublicKeyResponse {
|
||||
/**
|
||||
* The provided backup auth credential presentation could not be
|
||||
* authenticated. Either, the presentation could not be verified, or
|
||||
* the public key signature was invalid, or there is no backup associated
|
||||
* with the backup-id in the presentation.
|
||||
*
|
||||
* This may also be returned if there was an existing public key and the
|
||||
* provided public key did not match.
|
||||
*/
|
||||
errors.FailedZkAuthentication failed_authentication = 1 [(tag.reason) = "failed_authentication"];
|
||||
}
|
||||
|
||||
message GetCdnCredentialsRequest {
|
||||
SignedPresentation signed_presentation = 1;
|
||||
int32 cdn = 2;
|
||||
}
|
||||
message GetCdnCredentialsResponse {
|
||||
/**
|
||||
* Headers to include with requests to the read from the backup CDN. Includes
|
||||
* time limited read-only credentials.
|
||||
*/
|
||||
map<string, string> headers = 1;
|
||||
message CdnCredentials {
|
||||
map<string, string> headers = 1;
|
||||
}
|
||||
oneof outcome {
|
||||
/**
|
||||
* Headers to include with requests to the read from the backup CDN. Includes
|
||||
* time limited read-only credentials.
|
||||
*/
|
||||
CdnCredentials cdn_credentials = 1;
|
||||
|
||||
/**
|
||||
* The provided backup auth credential presentation could not be
|
||||
* authenticated. Either, the presentation could not be verified, or
|
||||
* the public key signature was invalid, or there is no backup associated
|
||||
* with the backup-id in the presentation.
|
||||
*/
|
||||
errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"];
|
||||
}
|
||||
}
|
||||
|
||||
message GetSvrBCredentialsRequest {
|
||||
SignedPresentation signed_presentation = 1;
|
||||
}
|
||||
message GetSvrBCredentialsResponse {
|
||||
/**
|
||||
* A username that can be presented to authenticate with SVRB
|
||||
*/
|
||||
string username = 1;
|
||||
|
||||
/**
|
||||
* A password that can be presented to authenticate with SVRB
|
||||
*/
|
||||
string password = 2;
|
||||
message GetSvrBCredentialsResponse {
|
||||
message SvrBCredentials {
|
||||
/**
|
||||
* A username that can be presented to authenticate with SVRB
|
||||
*/
|
||||
string username = 1;
|
||||
|
||||
/**
|
||||
* A password that can be presented to authenticate with SVRB
|
||||
*/
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
oneof outcome {
|
||||
SvrBCredentials svrb_credentials = 1;
|
||||
|
||||
/**
|
||||
* The provided backup auth credential presentation could not be
|
||||
* authenticated. Either, the presentation could not be verified, or
|
||||
* the public key signature was invalid, or there is no backup associated
|
||||
* with the backup-id in the presentation.
|
||||
*/
|
||||
errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"];
|
||||
}
|
||||
}
|
||||
|
||||
message GetBackupInfoRequest {
|
||||
SignedPresentation signed_presentation = 1;
|
||||
}
|
||||
message GetBackupInfoResponse {
|
||||
/**
|
||||
* The base directory of your backup data on the cdn. The message backup can
|
||||
* be found in the returned cdn at /backup_dir/backup_name and stored media can
|
||||
* be found at /backup_dir/media_dir/media_id
|
||||
*/
|
||||
string backup_dir = 1;
|
||||
message BackupInfo {
|
||||
/**
|
||||
* The base directory of your backup data on the cdn. The message backup can
|
||||
* be found in the returned cdn at /backup_dir/backup_name and stored media can
|
||||
* be found at /backup_dir/media_dir/media_id
|
||||
*/
|
||||
string backup_dir = 1;
|
||||
|
||||
/**
|
||||
* The prefix path component for media objects on a cdn. Stored media for a
|
||||
* media_id can be found at /backup_dir/media_dir/media_id, where the media_id
|
||||
* is encoded in unpadded url-safe base64.
|
||||
*/
|
||||
string media_dir = 2;
|
||||
/**
|
||||
* The prefix path component for media objects on a cdn. Stored media for a
|
||||
* media_id can be found at /backup_dir/media_dir/media_id, where the media_id
|
||||
* is encoded in unpadded url-safe base64.
|
||||
*/
|
||||
string media_dir = 2;
|
||||
|
||||
/**
|
||||
* The CDN type where the message backup is stored. Media may be stored
|
||||
* elsewhere. If absent, no message backup currently exists.
|
||||
*/
|
||||
optional int32 cdn = 3;
|
||||
/**
|
||||
* The CDN type where the message backup is stored. Media may be stored
|
||||
* elsewhere. If absent, no message backup currently exists.
|
||||
*/
|
||||
optional int32 cdn = 3;
|
||||
|
||||
/**
|
||||
* The name of the most recent message backup on the cdn. The backup is at
|
||||
* /backup_dir/backup_name. If absent, no message backup currently exists.
|
||||
*/
|
||||
optional string backup_name = 4;
|
||||
/**
|
||||
* The name of the most recent message backup on the cdn. The backup is at
|
||||
* /backup_dir/backup_name. If absent, no message backup currently exists.
|
||||
*/
|
||||
optional string backup_name = 4;
|
||||
|
||||
/**
|
||||
* The amount of space used to store media
|
||||
*/
|
||||
uint64 used_space = 5;
|
||||
/**
|
||||
* The amount of space used to store media
|
||||
*/
|
||||
uint64 used_space = 5;
|
||||
}
|
||||
|
||||
oneof outcome {
|
||||
BackupInfo backup_info = 1;
|
||||
|
||||
/**
|
||||
* The provided backup auth credential presentation could not be
|
||||
* authenticated. Either, the presentation could not be verified, or
|
||||
* the public key signature was invalid, or there is no backup associated
|
||||
* with the backup-id in the presentation.
|
||||
*/
|
||||
errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"];
|
||||
}
|
||||
}
|
||||
|
||||
message RefreshRequest {
|
||||
SignedPresentation signed_presentation = 1;
|
||||
}
|
||||
message RefreshResponse {
|
||||
SignedPresentation signed_presentation = 1;
|
||||
oneof outcome {
|
||||
/**
|
||||
* The provided backup auth credential presentation could not be
|
||||
* authenticated. Either, the presentation could not be verified, or
|
||||
* the public key signature was invalid, or there is no backup associated
|
||||
* with the backup-id in the presentation.
|
||||
*/
|
||||
errors.FailedZkAuthentication failed_authentication = 1 [(tag.reason) = "failed_authentication"];
|
||||
}
|
||||
}
|
||||
|
||||
message GetUploadFormRequest {
|
||||
@@ -382,26 +430,46 @@ message GetUploadFormRequest {
|
||||
}
|
||||
}
|
||||
message GetUploadFormResponse {
|
||||
/**
|
||||
* Indicates the CDN type. 3 indicates resumable uploads using TUS
|
||||
*/
|
||||
int32 cdn = 1;
|
||||
message UploadForm {
|
||||
/**
|
||||
* Indicates the CDN type. 3 indicates resumable uploads using TUS
|
||||
*/
|
||||
int32 cdn = 1;
|
||||
|
||||
/**
|
||||
* The location within the specified cdn where the finished upload can be found
|
||||
*/
|
||||
string key = 2;
|
||||
/**
|
||||
* The location within the specified cdn where the finished upload can be found
|
||||
*/
|
||||
string key = 2;
|
||||
|
||||
/**
|
||||
* A map of headers to include with all upload requests. Potentially contains
|
||||
* time-limited upload credentials
|
||||
*/
|
||||
map<string, string> headers = 3;
|
||||
/**
|
||||
* A map of headers to include with all upload requests. Potentially contains
|
||||
* time-limited upload credentials
|
||||
*/
|
||||
map<string, string> headers = 3;
|
||||
|
||||
/**
|
||||
* The URL to upload to with the appropriate protocol
|
||||
*/
|
||||
string signed_upload_location = 4;
|
||||
/**
|
||||
* The URL to upload to with the appropriate protocol
|
||||
*/
|
||||
string signed_upload_location = 4;
|
||||
}
|
||||
|
||||
oneof outcome {
|
||||
UploadForm upload_form = 1;
|
||||
|
||||
/**
|
||||
* The provided backup auth credential presentation could not be
|
||||
* authenticated. Either, the presentation could not be verified, or
|
||||
* the public key signature was invalid, or there is no backup associated
|
||||
* with the backup-id in the presentation.
|
||||
*/
|
||||
errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"];
|
||||
|
||||
/**
|
||||
* The request size was larger than the maximum supported upload size. The
|
||||
* maximum upload size is subject to change.
|
||||
*/
|
||||
errors.FailedPrecondition exceeds_max_upload_length = 3 [(tag.reason) = "oversize_upload"];
|
||||
}
|
||||
}
|
||||
|
||||
message CopyMediaItem {
|
||||
@@ -468,20 +536,28 @@ message CopyMediaResponse {
|
||||
*/
|
||||
CopySuccess success = 2;
|
||||
|
||||
/**
|
||||
* The provided backup auth credential presentation could not be
|
||||
* authenticated. Either, the presentation could not be verified, or
|
||||
* the public key signature was invalid, or there is no backup associated
|
||||
* with the backup-id in the presentation.
|
||||
*/
|
||||
errors.FailedZkAuthentication failed_authentication = 3 [(tag.reason) = "failed_authentication"];
|
||||
|
||||
/**
|
||||
* The source object was not found
|
||||
*/
|
||||
SourceNotFound source_not_found = 3;
|
||||
SourceNotFound source_not_found = 4;
|
||||
|
||||
/**
|
||||
* The provided object length was incorrect
|
||||
*/
|
||||
WrongSourceLength wrong_source_length = 4;
|
||||
WrongSourceLength wrong_source_length = 5;
|
||||
|
||||
/**
|
||||
* All media capacity has been consumed. Free some space to continue.
|
||||
*/
|
||||
OutOfSpace out_of_space = 5;
|
||||
OutOfSpace out_of_space = 6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,36 +590,61 @@ message ListMediaResponse {
|
||||
uint64 length = 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* A page of media objects stored for this backup ID
|
||||
*/
|
||||
repeated ListEntry page = 1;
|
||||
message ListResult {
|
||||
|
||||
/**
|
||||
* The base directory of the backup data on the cdn. The stored media can be
|
||||
* found at /backup_dir/media_dir/media_id, where the media_id is encoded with
|
||||
* unpadded url-safe base64.
|
||||
*/
|
||||
string backup_dir = 2;
|
||||
/**
|
||||
* A page of media objects stored for this backup ID
|
||||
*/
|
||||
repeated ListEntry page = 1;
|
||||
|
||||
/**
|
||||
* The prefix path component for the media objects. The stored media for
|
||||
* media_id can be found at /backup_dir/media_dir/media_id, where the media_id
|
||||
* is encoded with unpadded url-safe base64.
|
||||
*/
|
||||
string media_dir = 3;
|
||||
/**
|
||||
* The base directory of the backup data on the cdn. The stored media can be
|
||||
* found at /backup_dir/media_dir/media_id, where the media_id is encoded with
|
||||
* unpadded url-safe base64.
|
||||
*/
|
||||
string backup_dir = 2;
|
||||
|
||||
/**
|
||||
* If set, the cursor value to pass to the next list request to continue
|
||||
* listing. If absent, all objects have been listed
|
||||
*/
|
||||
optional string cursor = 4;
|
||||
/**
|
||||
* The prefix path component for the media objects. The stored media for
|
||||
* media_id can be found at /backup_dir/media_dir/media_id, where the media_id
|
||||
* is encoded with unpadded url-safe base64.
|
||||
*/
|
||||
string media_dir = 3;
|
||||
|
||||
/**
|
||||
* If set, the cursor value to pass to the next list request to continue
|
||||
* listing. If absent, all objects have been listed
|
||||
*/
|
||||
optional string cursor = 4;
|
||||
}
|
||||
|
||||
oneof outcome {
|
||||
ListResult list_result = 1;
|
||||
|
||||
/**
|
||||
* The provided backup auth credential presentation could not be
|
||||
* authenticated. Either, the presentation could not be verified, or
|
||||
* the public key signature was invalid, or there is no backup associated
|
||||
* with the backup-id in the presentation.
|
||||
*/
|
||||
errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"];
|
||||
}
|
||||
}
|
||||
|
||||
message DeleteAllRequest {
|
||||
SignedPresentation signed_presentation = 1;
|
||||
}
|
||||
message DeleteAllResponse {}
|
||||
message DeleteAllResponse {
|
||||
oneof outcome {
|
||||
/**
|
||||
* The provided backup auth credential presentation could not be
|
||||
* authenticated. Either, the presentation could not be verified, or
|
||||
* the public key signature was invalid, or there is no backup associated
|
||||
* with the backup-id in the presentation.
|
||||
*/
|
||||
errors.FailedZkAuthentication failed_authentication = 1 [(tag.reason) = "failed_authentication"];
|
||||
}
|
||||
}
|
||||
|
||||
message DeleteMediaItem {
|
||||
/**
|
||||
@@ -564,13 +665,15 @@ message DeleteMediaRequest {
|
||||
}
|
||||
|
||||
message DeleteMediaResponse {
|
||||
/**
|
||||
* The backup cdn where the media object was stored
|
||||
*/
|
||||
int32 cdn = 1;
|
||||
oneof outcome {
|
||||
DeleteMediaItem deleted_item = 1;
|
||||
|
||||
/**
|
||||
* The media_id of the object that was successfully deleted
|
||||
*/
|
||||
bytes media_id = 3;
|
||||
/**
|
||||
* The provided backup auth credential presentation could not be
|
||||
* authenticated. Either, the presentation could not be verified, or
|
||||
* the public key signature was invalid, or there is no backup associated
|
||||
* with the backup-id in the presentation.
|
||||
*/
|
||||
errors.FailedZkAuthentication failed_authentication = 2 [(tag.reason) = "failed_authentication"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,19 @@ message NotFound {}
|
||||
* met. For example, if there was a request to update foo, but foo had not been
|
||||
* set, this would be an appropriate error.
|
||||
*/
|
||||
|
||||
message FailedPrecondition {
|
||||
/**
|
||||
* An optional description indicating what precondition failed.
|
||||
*/
|
||||
string description = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response message that authentication via an anonymous credential failed.
|
||||
*/
|
||||
message FailedZkAuthentication {
|
||||
/**
|
||||
* An optional description with additional information about the failure.
|
||||
*/
|
||||
string description = 1;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -21,8 +20,6 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
@@ -107,7 +104,7 @@ public class BackupAuthManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void commitBackupId() throws RateLimitExceededException {
|
||||
void commitBackupId() throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
|
||||
final BackupAuthManager authManager = create();
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
@@ -158,15 +155,13 @@ public class BackupAuthManagerTest {
|
||||
linkedDevice(),
|
||||
Optional.of(backupAuthTestUtil.getRequest(messagesBackupKey, aci)),
|
||||
Optional.of(backupAuthTestUtil.getRequest(mediaBackupKey, aci)));
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(commit)
|
||||
.extracting(ex -> ex.getStatus().getCode())
|
||||
.isEqualTo(Status.Code.PERMISSION_DENIED);
|
||||
assertThatExceptionOfType(BackupPermissionException.class)
|
||||
.isThrownBy(commit);
|
||||
}
|
||||
|
||||
@CartesianTest
|
||||
void paidTierCredentialViaConfiguration(@CartesianTest.Enum final BackupCredentialType credentialType)
|
||||
throws VerificationFailedException {
|
||||
throws VerificationFailedException, BackupNotFoundException {
|
||||
final BackupAuthManager authManager = create(BackupLevel.PAID, rateLimiter(aci, false, false));
|
||||
|
||||
final byte[] backupKey = switch (credentialType) {
|
||||
@@ -196,7 +191,7 @@ public class BackupAuthManagerTest {
|
||||
|
||||
@CartesianTest
|
||||
void getBackupAuthCredentials(@CartesianTest.Enum final BackupLevel backupLevel,
|
||||
@CartesianTest.Enum final BackupCredentialType credentialType) {
|
||||
@CartesianTest.Enum final BackupCredentialType credentialType) throws BackupNotFoundException {
|
||||
|
||||
final BackupAuthManager authManager = create();
|
||||
|
||||
@@ -217,16 +212,14 @@ public class BackupAuthManagerTest {
|
||||
|
||||
final Account account = new MockAccountBuilder().build();
|
||||
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() ->
|
||||
authManager.getBackupAuthCredentials(account, credentialType, range(Duration.ofDays(1))))
|
||||
.extracting(ex -> ex.getStatus().getCode())
|
||||
.isEqualTo(Status.Code.NOT_FOUND);
|
||||
assertThatExceptionOfType(BackupNotFoundException.class)
|
||||
.isThrownBy(() -> authManager.getBackupAuthCredentials(account, credentialType, range(Duration.ofDays(1))));
|
||||
}
|
||||
|
||||
@CartesianTest
|
||||
void getReceiptCredentials(@CartesianTest.Enum final BackupLevel backupLevel,
|
||||
@CartesianTest.Enum final BackupCredentialType credentialType) throws VerificationFailedException {
|
||||
@CartesianTest.Enum final BackupCredentialType credentialType)
|
||||
throws VerificationFailedException, BackupNotFoundException {
|
||||
final BackupAuthManager authManager = create();
|
||||
|
||||
final byte[] backupKey = switch (credentialType) {
|
||||
@@ -259,7 +252,7 @@ public class BackupAuthManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void expiringBackupPayment() throws VerificationFailedException {
|
||||
void expiringBackupPayment() throws VerificationFailedException, BackupNotFoundException {
|
||||
clock.pin(Instant.ofEpochSecond(1));
|
||||
final Instant day4 = Instant.EPOCH.plus(Duration.ofDays(4));
|
||||
|
||||
@@ -292,7 +285,7 @@ public class BackupAuthManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void expiredBackupPayment() {
|
||||
void expiredBackupPayment() throws BackupNotFoundException {
|
||||
final Instant day1 = Instant.EPOCH.plus(Duration.ofDays(1));
|
||||
final Instant day2 = Instant.EPOCH.plus(Duration.ofDays(2));
|
||||
final Instant day3 = Instant.EPOCH.plus(Duration.ofDays(3));
|
||||
@@ -335,7 +328,8 @@ public class BackupAuthManagerTest {
|
||||
|
||||
|
||||
@Test
|
||||
void redeemReceipt() throws InvalidInputException, VerificationFailedException {
|
||||
void redeemReceipt()
|
||||
throws InvalidInputException, VerificationFailedException, BackupInvalidArgumentException, BackupMissingIdCommitmentException, BackupBadReceiptException {
|
||||
final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1));
|
||||
final BackupAuthManager authManager = create();
|
||||
final Account account = new MockAccountBuilder()
|
||||
@@ -358,15 +352,13 @@ public class BackupAuthManagerTest {
|
||||
clock.pin(Instant.EPOCH.plus(Duration.ofDays(1)));
|
||||
when(redeemedReceiptsManager.put(any(), eq(expirationTime.getEpochSecond()), eq(201L), eq(aci)))
|
||||
.thenReturn(CompletableFuture.completedFuture(true));
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() ->
|
||||
authManager.redeemReceipt(account, receiptPresentation(201, expirationTime)))
|
||||
.extracting(ex -> ex.getStatus().getCode())
|
||||
.isEqualTo(Status.Code.ABORTED);
|
||||
assertThatExceptionOfType(BackupMissingIdCommitmentException.class)
|
||||
.isThrownBy(() -> authManager.redeemReceipt(account, receiptPresentation(201, expirationTime)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeRedemptions() throws InvalidInputException, VerificationFailedException {
|
||||
void mergeRedemptions()
|
||||
throws InvalidInputException, VerificationFailedException, BackupInvalidArgumentException, BackupMissingIdCommitmentException, BackupBadReceiptException {
|
||||
final Instant newExpirationTime = Instant.EPOCH.plus(Duration.ofDays(1));
|
||||
final Instant existingExpirationTime = Instant.EPOCH.plus(Duration.ofDays(1)).plus(Duration.ofSeconds(1));
|
||||
|
||||
@@ -396,10 +388,8 @@ public class BackupAuthManagerTest {
|
||||
final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1));
|
||||
clock.pin(expirationTime.plus(Duration.ofSeconds(1)));
|
||||
final BackupAuthManager authManager = create();
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), receiptPresentation(3, expirationTime)))
|
||||
.extracting(ex -> ex.getStatus().getCode())
|
||||
.isEqualTo(Status.Code.INVALID_ARGUMENT);
|
||||
assertThatExceptionOfType(BackupBadReceiptException.class)
|
||||
.isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), receiptPresentation(3, expirationTime)));
|
||||
verifyNoInteractions(accountsManager);
|
||||
verifyNoInteractions(redeemedReceiptsManager);
|
||||
}
|
||||
@@ -410,11 +400,8 @@ public class BackupAuthManagerTest {
|
||||
final Instant expirationTime = Instant.EPOCH.plus(Duration.ofDays(1));
|
||||
clock.pin(expirationTime.plus(Duration.ofSeconds(1)));
|
||||
final BackupAuthManager authManager = create();
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() ->
|
||||
authManager.redeemReceipt(mock(Account.class), receiptPresentation(level, expirationTime)))
|
||||
.extracting(ex -> ex.getStatus().getCode())
|
||||
.isEqualTo(Status.Code.INVALID_ARGUMENT);
|
||||
assertThatExceptionOfType(BackupBadReceiptException.class)
|
||||
.isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), receiptPresentation(level, expirationTime)));
|
||||
verifyNoInteractions(accountsManager);
|
||||
verifyNoInteractions(redeemedReceiptsManager);
|
||||
}
|
||||
@@ -423,10 +410,8 @@ public class BackupAuthManagerTest {
|
||||
void redeemInvalidPresentation() throws InvalidInputException, VerificationFailedException {
|
||||
final BackupAuthManager authManager = create();
|
||||
final ReceiptCredentialPresentation invalid = receiptPresentation(ServerSecretParams.generate(), 3L, Instant.EPOCH);
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), invalid))
|
||||
.extracting(ex -> ex.getStatus().getCode())
|
||||
.isEqualTo(Status.Code.INVALID_ARGUMENT);
|
||||
assertThatExceptionOfType(BackupBadReceiptException.class)
|
||||
.isThrownBy(() -> authManager.redeemReceipt(mock(Account.class), invalid));
|
||||
verifyNoInteractions(accountsManager);
|
||||
verifyNoInteractions(redeemedReceiptsManager);
|
||||
}
|
||||
@@ -444,10 +429,8 @@ public class BackupAuthManagerTest {
|
||||
when(redeemedReceiptsManager.put(any(), eq(expirationTime.getEpochSecond()), eq(201L), eq(aci)))
|
||||
.thenReturn(CompletableFuture.completedFuture(false));
|
||||
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> authManager.redeemReceipt(account, receiptPresentation(201, expirationTime)))
|
||||
.extracting(ex -> ex.getStatus().getCode())
|
||||
.isEqualTo(Status.Code.INVALID_ARGUMENT);
|
||||
assertThatExceptionOfType(BackupBadReceiptException.class)
|
||||
.isThrownBy(() -> authManager.redeemReceipt(account, receiptPresentation(201, expirationTime)));
|
||||
verifyNoInteractions(accountsManager);
|
||||
}
|
||||
|
||||
@@ -544,10 +527,8 @@ public class BackupAuthManagerTest {
|
||||
authManager.commitBackupId(account, primaryDevice(), newMessagesCredential, newMediaCredential);
|
||||
|
||||
if (messageChange == CredentialChangeType.NO_UPDATE && mediaChange == CredentialChangeType.NO_UPDATE) {
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(commit)
|
||||
.extracting(ex -> ex.getStatus().getCode())
|
||||
.isEqualTo(Status.Code.INVALID_ARGUMENT);
|
||||
assertThatExceptionOfType(BackupInvalidArgumentException.class)
|
||||
.isThrownBy(commit);
|
||||
} else if (expectRateLimit) {
|
||||
assertThatExceptionOfType(RateLimitExceededException.class).isThrownBy(commit);
|
||||
} else {
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
|
||||
@@ -76,6 +77,10 @@ public class BackupAuthTestUtil {
|
||||
});
|
||||
final RedemptionRange redemptionRange;
|
||||
redemptionRange = RedemptionRange.inclusive(clock, redemptionStart, redemptionEnd);
|
||||
return issuer.getBackupAuthCredentials(account, credentialType, redemptionRange);
|
||||
try {
|
||||
return issuer.getBackupAuthCredentials(account, credentialType, redemptionRange);
|
||||
} catch (BackupNotFoundException e) {
|
||||
return Assertions.fail("Backup credential request not found even though we set one");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ import static org.mockito.Mockito.when;
|
||||
import static org.whispersystems.textsecuregcm.util.MockUtils.randomSecretBytes;
|
||||
|
||||
import io.dropwizard.util.DataSize;
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -184,11 +182,7 @@ public class BackupManagerTest {
|
||||
() -> BackupManager.checkBackupLevel(backupUser, requiredLevel);
|
||||
|
||||
if (expectException) {
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(checkBackupLevel)
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.Code.PERMISSION_DENIED);
|
||||
assertThatExceptionOfType(BackupPermissionException.class).isThrownBy(checkBackupLevel);
|
||||
} else {
|
||||
assertThatNoException().isThrownBy(checkBackupLevel);
|
||||
}
|
||||
@@ -212,11 +206,7 @@ public class BackupManagerTest {
|
||||
() -> BackupManager.checkBackupCredentialType(backupUser, requiredCredentialType);
|
||||
|
||||
if (expectException) {
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(checkCredentialType)
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.Code.UNAUTHENTICATED);
|
||||
assertThatExceptionOfType(BackupWrongCredentialTypeException.class).isThrownBy(checkCredentialType);
|
||||
} else {
|
||||
assertThatNoException().isThrownBy(checkCredentialType);
|
||||
}
|
||||
@@ -224,7 +214,7 @@ public class BackupManagerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource
|
||||
public void createBackup(final BackupLevel backupLevel) {
|
||||
public void createBackup(final BackupLevel backupLevel) throws BackupException {
|
||||
|
||||
final Instant now = Instant.ofEpochSecond(Duration.ofDays(1).getSeconds());
|
||||
testClock.pin(now);
|
||||
@@ -253,9 +243,8 @@ public class BackupManagerTest {
|
||||
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, backupLevel);
|
||||
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> backupManager.createMessageBackupUploadDescriptor(backupUser))
|
||||
.matches(exception -> exception.getStatus().getCode() == Status.UNAUTHENTICATED.getCode());
|
||||
assertThatExceptionOfType(BackupWrongCredentialTypeException.class)
|
||||
.isThrownBy(() -> backupManager.createMessageBackupUploadDescriptor(backupUser));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -270,26 +259,20 @@ public class BackupManagerTest {
|
||||
@Test
|
||||
public void createTemporaryMediaAttachmentWrongTier() {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.FREE);
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.Code.PERMISSION_DENIED);
|
||||
assertThatExceptionOfType(BackupPermissionException.class)
|
||||
.isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createTemporaryMediaAttachmentWrongCredentialType() {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.Code.UNAUTHENTICATED);
|
||||
assertThatExceptionOfType(BackupWrongCredentialTypeException.class)
|
||||
.isThrownBy(() -> backupManager.createTemporaryAttachmentUploadDescriptor(backupUser));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource
|
||||
public void ttlRefresh(final BackupLevel backupLevel) {
|
||||
public void ttlRefresh(final BackupLevel backupLevel) throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, backupLevel);
|
||||
|
||||
final Instant tstart = Instant.ofEpochSecond(1).plus(Duration.ofDays(1));
|
||||
@@ -311,7 +294,7 @@ public class BackupManagerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource
|
||||
public void createBackupRefreshesTtl(final BackupLevel backupLevel) {
|
||||
public void createBackupRefreshesTtl(final BackupLevel backupLevel) throws BackupException {
|
||||
final Instant tstart = Instant.ofEpochSecond(1).plus(Duration.ofDays(1));
|
||||
final Instant tnext = tstart.plus(Duration.ofDays(1));
|
||||
|
||||
@@ -340,19 +323,16 @@ public class BackupManagerTest {
|
||||
final ECKeyPair keyPair = ECKeyPair.generate();
|
||||
|
||||
// haven't set a public key yet, but should fail before hitting the database anyway
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
|
||||
.isThrownBy(() -> backupManager.authenticateBackupUser(
|
||||
invalidPresentation,
|
||||
keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize()),
|
||||
null))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||
null));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void invalidPresentationCorrectSignature() throws VerificationFailedException {
|
||||
public void invalidPresentationCorrectSignature() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||
BackupLevel.FREE, backupKey, aci);
|
||||
final BackupAuthCredentialPresentation invalidPresentation = backupAuthTestUtil.getPresentation(
|
||||
@@ -365,14 +345,11 @@ public class BackupManagerTest {
|
||||
keyPair.getPrivateKey().calculateSignature(presentation.serialize()),
|
||||
keyPair.getPublicKey());
|
||||
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
|
||||
.isThrownBy(() -> backupManager.authenticateBackupUser(
|
||||
invalidPresentation,
|
||||
keyPair.getPrivateKey().calculateSignature(invalidPresentation.serialize()),
|
||||
null))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||
null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -384,15 +361,12 @@ public class BackupManagerTest {
|
||||
final byte[] signature = keyPair.getPrivateKey().calculateSignature(presentation.serialize());
|
||||
|
||||
// haven't set a public key yet
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> backupManager.authenticateBackupUser(presentation, signature, null))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
|
||||
.isThrownBy(() -> backupManager.authenticateBackupUser(presentation, signature, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mismatchedPublicKey() throws VerificationFailedException {
|
||||
public void mismatchedPublicKey() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||
BackupLevel.FREE, backupKey, aci);
|
||||
|
||||
@@ -404,18 +378,15 @@ public class BackupManagerTest {
|
||||
backupManager.setPublicKey(presentation, signature1, keyPair1.getPublicKey());
|
||||
|
||||
// shouldn't be able to set a different public key
|
||||
assertThatExceptionOfType(StatusRuntimeException.class).isThrownBy(() ->
|
||||
backupManager.setPublicKey(presentation, signature2, keyPair2.getPublicKey()))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
|
||||
.isThrownBy(() -> backupManager.setPublicKey(presentation, signature2, keyPair2.getPublicKey()));
|
||||
|
||||
// should be able to set the same public key again (noop)
|
||||
backupManager.setPublicKey(presentation, signature1, keyPair1.getPublicKey());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void signatureValidation() throws VerificationFailedException {
|
||||
public void signatureValidation() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||
BackupLevel.FREE, backupKey, aci);
|
||||
|
||||
@@ -427,19 +398,14 @@ public class BackupManagerTest {
|
||||
wrongSignature[1] += 1;
|
||||
|
||||
// shouldn't be able to set a public key with an invalid signature
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> backupManager.setPublicKey(presentation, wrongSignature, keyPair.getPublicKey()))
|
||||
.extracting(ex -> ex.getStatus().getCode())
|
||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
|
||||
.isThrownBy(() -> backupManager.setPublicKey(presentation, wrongSignature, keyPair.getPublicKey()));
|
||||
|
||||
backupManager.setPublicKey(presentation, signature, keyPair.getPublicKey());
|
||||
|
||||
// shouldn't be able to authenticate with an invalid signature
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> backupManager.authenticateBackupUser(presentation, wrongSignature, null))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
|
||||
.isThrownBy(() -> backupManager.authenticateBackupUser(presentation, wrongSignature, null));
|
||||
|
||||
// correct signature
|
||||
final AuthenticatedBackupUser user = backupManager.authenticateBackupUser(presentation, signature, null);
|
||||
@@ -448,7 +414,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void credentialExpiration() throws VerificationFailedException {
|
||||
public void credentialExpiration() throws VerificationFailedException, BackupException {
|
||||
|
||||
// credential for 1 day after epoch
|
||||
testClock.pin(Instant.ofEpochSecond(1).plus(Duration.ofDays(1)));
|
||||
@@ -468,15 +434,12 @@ public class BackupManagerTest {
|
||||
|
||||
// should be rejected the day after that
|
||||
testClock.pin(Instant.ofEpochSecond(1).plus(Duration.ofDays(3)));
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature, null))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.UNAUTHENTICATED.getCode());
|
||||
assertThatExceptionOfType(BackupFailedZkAuthenticationException.class)
|
||||
.isThrownBy(() -> backupManager.authenticateBackupUser(oldCredential, signature, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copySuccess() {
|
||||
public void copySuccess() throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
final CopyResult copied = copy(backupUser);
|
||||
|
||||
@@ -493,7 +456,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copyUsageCheckpoints() throws InterruptedException {
|
||||
public void copyUsageCheckpoints() throws InterruptedException, BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
backupsDb.setMediaUsage(backupUser, new UsageInfo(0, 0)).join();
|
||||
|
||||
@@ -512,7 +475,7 @@ public class BackupManagerTest {
|
||||
.thenReturn(slow);
|
||||
final ArrayBlockingQueue<CopyResult> copyResults = new ArrayBlockingQueue<>(100);
|
||||
final CompletableFuture<Void> future = backupManager
|
||||
.copyToBackup(backupUser, toCopy)
|
||||
.copyToBackup(backupManager.getCopyQuota(backupUser, toCopy))
|
||||
.doOnNext(copyResults::add).then().toFuture();
|
||||
|
||||
for (int i = 0; i < slowIndex; i++) {
|
||||
@@ -540,7 +503,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copyFailure() {
|
||||
public void copyFailure() throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
assertThat(copyError(backupUser, new SourceObjectNotFoundException()).outcome())
|
||||
.isEqualTo(CopyResult.Outcome.SOURCE_NOT_FOUND);
|
||||
@@ -552,7 +515,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void copyPartialSuccess() {
|
||||
public void copyPartialSuccess() throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
final List<CopyParameters> toCopy = List.of(
|
||||
new CopyParameters(3, "success", 100, COPY_ENCRYPTION_PARAM, TestRandomUtil.nextBytes(15)),
|
||||
@@ -568,7 +531,7 @@ public class BackupManagerTest {
|
||||
when(remoteStorageManager.copy(eq(3), eq("badlength"), eq(300), any(), any()))
|
||||
.thenReturn(CompletableFuture.failedFuture(new InvalidLengthException("")));
|
||||
|
||||
final List<CopyResult> results = backupManager.copyToBackup(backupUser, toCopy)
|
||||
final List<CopyResult> results = backupManager.copyToBackup(backupManager.getCopyQuota(backupUser, toCopy))
|
||||
.collectList().block();
|
||||
|
||||
assertThat(results).hasSize(3);
|
||||
@@ -587,15 +550,11 @@ public class BackupManagerTest {
|
||||
public void copyWrongCredentialType() {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
|
||||
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> copy(backupUser))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.Code.UNAUTHENTICATED);
|
||||
assertThatExceptionOfType(BackupWrongCredentialTypeException.class).isThrownBy(() -> copy(backupUser));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void quotaEnforcementNoRecalculation() {
|
||||
public void quotaEnforcementNoRecalculation() throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
verifyNoInteractions(remoteStorageManager);
|
||||
|
||||
@@ -612,7 +571,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void quotaEnforcementRecalculation() {
|
||||
public void quotaEnforcementRecalculation() throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
final String backupMediaPrefix = "%s/%s/".formatted(backupUser.backupDir(), backupUser.mediaDir());
|
||||
|
||||
@@ -642,7 +601,7 @@ public class BackupManagerTest {
|
||||
public void quotaEnforcement(
|
||||
@CartesianTest.Values(booleans = {true, false}) boolean hasSpaceBeforeRecalc,
|
||||
@CartesianTest.Values(booleans = {true, false}) boolean hasSpaceAfterRecalc,
|
||||
@CartesianTest.Values(booleans = {true, false}) boolean doesReaclc) {
|
||||
@CartesianTest.Values(booleans = {true, false}) boolean doesReaclc) throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
final String backupMediaPrefix = "%s/%s/".formatted(backupUser.backupDir(), backupUser.mediaDir());
|
||||
|
||||
@@ -701,7 +660,7 @@ public class BackupManagerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", "cursor"})
|
||||
public void list(final String cursorVal) {
|
||||
public void list(final String cursorVal) throws BackupException {
|
||||
final Optional<String> cursor = Optional.of(cursorVal).filter(StringUtils::isNotBlank);
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
|
||||
final String backupMediaPrefix = "%s/%s/".formatted(backupUser.backupDir(), backupUser.mediaDir());
|
||||
@@ -724,7 +683,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteEntireBackup() {
|
||||
public void deleteEntireBackup() throws BackupException {
|
||||
final AuthenticatedBackupUser original = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
|
||||
|
||||
testClock.pin(Instant.ofEpochSecond(10));
|
||||
@@ -762,7 +721,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delete() {
|
||||
public void delete() throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
final byte[] mediaId = TestRandomUtil.nextBytes(16);
|
||||
final String backupMediaKey = "%s/%s/%s".formatted(
|
||||
@@ -786,10 +745,8 @@ public class BackupManagerTest {
|
||||
public void deleteWrongCredentialType() {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
|
||||
final byte[] mediaId = TestRandomUtil.nextBytes(16);
|
||||
assertThatThrownBy(() ->
|
||||
backupManager.deleteMedia(backupUser, List.of(new BackupManager.StorageDescriptor(5, mediaId))).then().block())
|
||||
.isInstanceOf(StatusRuntimeException.class)
|
||||
.matches(e -> ((StatusRuntimeException) e).getStatus().getCode() == Status.UNAUTHENTICATED.getCode());
|
||||
assertThatExceptionOfType(BackupWrongCredentialTypeException.class)
|
||||
.isThrownBy(() -> backupManager.deleteMedia(backupUser, List.of(new BackupManager.StorageDescriptor(5, mediaId))).then().block());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -797,14 +754,12 @@ public class BackupManagerTest {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
final BackupManager.StorageDescriptor sd = new BackupManager.StorageDescriptor(4, TestRandomUtil.nextBytes(15));
|
||||
when(remoteStorageManager.cdnNumber()).thenReturn(5);
|
||||
assertThatThrownBy(() ->
|
||||
backupManager.deleteMedia(backupUser, List.of(sd)).then().block())
|
||||
.isInstanceOf(StatusRuntimeException.class)
|
||||
.matches(e -> ((StatusRuntimeException) e).getStatus().getCode() == Status.INVALID_ARGUMENT.getCode());
|
||||
assertThatThrownBy(() -> backupManager.deleteMedia(backupUser, List.of(sd)).then().toFuture().join())
|
||||
.hasCauseInstanceOf(BackupInvalidArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteUsageCheckpoints() throws InterruptedException {
|
||||
public void deleteUsageCheckpoints() throws InterruptedException, BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA,
|
||||
BackupLevel.PAID);
|
||||
|
||||
@@ -849,7 +804,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deletePartialFailure() {
|
||||
public void deletePartialFailure() throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
|
||||
final List<BackupManager.StorageDescriptor> descriptors = new ArrayList<>();
|
||||
@@ -887,7 +842,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void alreadyDeleted() {
|
||||
public void alreadyDeleted() throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, BackupLevel.PAID);
|
||||
final byte[] mediaId = TestRandomUtil.nextBytes(16);
|
||||
final String backupMediaKey = "%s/%s/%s".formatted(
|
||||
@@ -907,7 +862,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listExpiredBackups() {
|
||||
public void listExpiredBackups() throws BackupException {
|
||||
final List<AuthenticatedBackupUser> backupUsers = IntStream.range(0, 10)
|
||||
.mapToObj(_ -> backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID))
|
||||
.toList();
|
||||
@@ -943,7 +898,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listExpiredBackupsByTier() {
|
||||
public void listExpiredBackupsByTier() throws BackupException {
|
||||
final byte[] backupId = TestRandomUtil.nextBytes(16);
|
||||
|
||||
// refreshed media timestamp at t=5
|
||||
@@ -971,7 +926,7 @@ public class BackupManagerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(mode = EnumSource.Mode.INCLUDE, names = {"MEDIA", "ALL"})
|
||||
public void expireBackup(ExpiredBackup.ExpirationType expirationType) {
|
||||
public void expireBackup(ExpiredBackup.ExpirationType expirationType) throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
|
||||
backupManager.createMessageBackupUploadDescriptor(backupUser);
|
||||
|
||||
@@ -1005,11 +960,9 @@ public class BackupManagerTest {
|
||||
|
||||
if (expirationType == ExpiredBackup.ExpirationType.ALL) {
|
||||
// should have deleted the db row for the backup
|
||||
assertThat(CompletableFutureTestUtil.assertFailsWithCause(
|
||||
StatusRuntimeException.class,
|
||||
backupsDb.describeBackup(backupUser))
|
||||
.getStatus().getCode())
|
||||
.isEqualTo(Status.NOT_FOUND.getCode());
|
||||
CompletableFutureTestUtil.assertFailsWithCause(
|
||||
BackupNotFoundException.class,
|
||||
backupsDb.describeBackup(backupUser));
|
||||
} else {
|
||||
// should have deleted all the media, but left the backup descriptor in place
|
||||
assertThatNoException().isThrownBy(() -> backupsDb.describeBackup(backupUser).join());
|
||||
@@ -1017,7 +970,7 @@ public class BackupManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteBackupPaginated() {
|
||||
public void deleteBackupPaginated() throws BackupException {
|
||||
final AuthenticatedBackupUser backupUser = backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, BackupLevel.PAID);
|
||||
backupManager.createMessageBackupUploadDescriptor(backupUser);
|
||||
|
||||
@@ -1055,7 +1008,7 @@ public class BackupManagerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(BackupLevel.class)
|
||||
void svrbAuthValid(BackupLevel backupLevel) {
|
||||
void svrbAuthValid(BackupLevel backupLevel) throws BackupException {
|
||||
testClock.pin(Instant.ofEpochSecond(123));
|
||||
final AuthenticatedBackupUser backupUser =
|
||||
backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MESSAGES, backupLevel);
|
||||
@@ -1072,29 +1025,26 @@ public class BackupManagerTest {
|
||||
// Can't use MEDIA for svrb auth
|
||||
final AuthenticatedBackupUser backupUser =
|
||||
backupUser(TestRandomUtil.nextBytes(16), BackupCredentialType.MEDIA, backupLevel);
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> backupManager.generateSvrbAuth(backupUser))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.Code.UNAUTHENTICATED);
|
||||
assertThatExceptionOfType(BackupWrongCredentialTypeException.class)
|
||||
.isThrownBy(() -> backupManager.generateSvrbAuth(backupUser));
|
||||
}
|
||||
|
||||
private CopyResult copyError(final AuthenticatedBackupUser backupUser, Throwable copyException) {
|
||||
private CopyResult copyError(final AuthenticatedBackupUser backupUser, Throwable copyException) throws BackupException {
|
||||
when(tusCredentialGenerator.generateUpload(any()))
|
||||
.thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
|
||||
when(remoteStorageManager.copy(eq(3), eq(COPY_PARAM.sourceKey()), eq(COPY_PARAM.sourceLength()), any(), any()))
|
||||
.thenReturn(CompletableFuture.failedFuture(copyException));
|
||||
return backupManager.copyToBackup(backupUser, List.of(COPY_PARAM)).single().block();
|
||||
return backupManager.copyToBackup(backupManager.getCopyQuota(backupUser, List.of(COPY_PARAM))).single().block();
|
||||
}
|
||||
|
||||
private CopyResult copy(final AuthenticatedBackupUser backupUser) {
|
||||
private CopyResult copy(final AuthenticatedBackupUser backupUser) throws BackupException {
|
||||
when(tusCredentialGenerator.generateUpload(any()))
|
||||
.thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
|
||||
when(tusCredentialGenerator.generateUpload(any()))
|
||||
.thenReturn(new BackupUploadDescriptor(3, "def", Collections.emptyMap(), ""));
|
||||
when(remoteStorageManager.copy(eq(3), eq(COPY_PARAM.sourceKey()), eq(COPY_PARAM.sourceLength()), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(null));
|
||||
return backupManager.copyToBackup(backupUser, List.of(COPY_PARAM)).single().block();
|
||||
return backupManager.copyToBackup(backupManager.getCopyQuota(backupUser, List.of(COPY_PARAM))).single().block();
|
||||
}
|
||||
|
||||
private static ExpiredBackup expiredBackup(final ExpiredBackup.ExpirationType expirationType,
|
||||
|
||||
@@ -8,8 +8,6 @@ package org.whispersystems.textsecuregcm.backup;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.grpc.Status;
|
||||
import io.grpc.StatusRuntimeException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
@@ -212,10 +210,9 @@ public class BackupsDbTest {
|
||||
backupsDb.finishExpiration(opt.get()).join();
|
||||
|
||||
// The backup entry should be gone
|
||||
assertThat(CompletableFutureTestUtil.assertFailsWithCause(StatusRuntimeException.class,
|
||||
backupsDb.describeBackup(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID)))
|
||||
.getStatus().getCode())
|
||||
.isEqualTo(Status.Code.NOT_FOUND);
|
||||
CompletableFutureTestUtil.assertFailsWithCause(
|
||||
BackupNotFoundException.class,
|
||||
backupsDb.describeBackup(backupUser(backupId, BackupCredentialType.MEDIA, BackupLevel.PAID)));
|
||||
assertThat(expiredBackups.apply(Instant.ofEpochSecond(10))).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import static org.mockito.Mockito.when;
|
||||
import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import io.grpc.Status;
|
||||
import jakarta.ws.rs.client.Entity;
|
||||
import jakarta.ws.rs.client.Invocation;
|
||||
import jakarta.ws.rs.client.WebTarget;
|
||||
@@ -34,7 +33,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.glassfish.jersey.server.ServerProperties;
|
||||
@@ -68,10 +66,16 @@ import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.RedemptionRange;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupAuthTestUtil;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupFailedZkAuthenticationException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupInvalidArgumentException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupManager;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupNotFoundException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupPermissionException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupUploadDescriptor;
|
||||
import org.whispersystems.textsecuregcm.backup.CopyResult;
|
||||
import org.whispersystems.textsecuregcm.entities.RemoteAttachment;
|
||||
import org.whispersystems.textsecuregcm.mappers.BackupExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.CompletionExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.GrpcStatusRuntimeExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
@@ -97,6 +101,7 @@ public class ArchiveControllerTest {
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
|
||||
.addProvider(new CompletionExceptionMapper())
|
||||
.addResource(new GrpcStatusRuntimeExceptionMapper())
|
||||
.addResource(new BackupExceptionMapper())
|
||||
.addProvider(new RateLimitExceededExceptionMapper())
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
@@ -165,7 +170,7 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setBackupId() throws RateLimitExceededException {
|
||||
public void setBackupId() throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
|
||||
final Response response = resources.getJerseyTest()
|
||||
.target("v1/archives/backupid")
|
||||
.request()
|
||||
@@ -183,7 +188,8 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setBackupIdPartial() throws RateLimitExceededException {
|
||||
public void setBackupIdPartial()
|
||||
throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
|
||||
final Response response = resources.getJerseyTest()
|
||||
.target("v1/archives/backupid")
|
||||
.request()
|
||||
@@ -302,13 +308,15 @@ public class ArchiveControllerTest {
|
||||
public static Stream<Arguments> setBackupIdException() {
|
||||
return Stream.of(
|
||||
Arguments.of(new RateLimitExceededException(null), 429),
|
||||
Arguments.of(Status.INVALID_ARGUMENT.withDescription("test").asRuntimeException(), 400)
|
||||
Arguments.of(new BackupInvalidArgumentException("test"), 400),
|
||||
Arguments.of(new BackupPermissionException("test"), 403)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
public void setBackupIdException(final Exception ex, final int expectedStatus) throws RateLimitExceededException {
|
||||
public void setBackupIdException(final Exception ex, final int expectedStatus)
|
||||
throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
|
||||
doThrow(ex).when(backupAuthManager).commitBackupId(any(), any(), any(), any());
|
||||
final Response response = resources.getJerseyTest()
|
||||
.target("v1/archives/backupid")
|
||||
@@ -322,7 +330,7 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCredentials() {
|
||||
public void getCredentials() throws BackupNotFoundException {
|
||||
final Instant start = Instant.now().truncatedTo(ChronoUnit.DAYS);
|
||||
final Instant end = start.plus(Duration.ofDays(1));
|
||||
final RedemptionRange expectedRange = RedemptionRange.inclusive(Clock.systemUTC(), start, end);
|
||||
@@ -331,9 +339,12 @@ public class ArchiveControllerTest {
|
||||
EnumMapUtil.toEnumMap(BackupCredentialType.class, credentialType -> backupAuthTestUtil.getCredentials(
|
||||
BackupLevel.PAID, backupAuthTestUtil.getRequest(messagesBackupKey, aci), credentialType, start, end));
|
||||
|
||||
expectedCredentialsByType.forEach((credentialType, expectedCredentials) ->
|
||||
for (Map.Entry<BackupCredentialType, List<BackupAuthManager.Credential>> entry : expectedCredentialsByType.entrySet()) {
|
||||
final BackupCredentialType credentialType = entry.getKey();
|
||||
final List<BackupAuthManager.Credential> expectedCredentials = entry.getValue();
|
||||
when(backupAuthManager.getBackupAuthCredentials(any(), eq(credentialType), eq(expectedRange)))
|
||||
.thenReturn(expectedCredentials));
|
||||
.thenReturn(expectedCredentials);
|
||||
}
|
||||
|
||||
final ArchiveController.BackupAuthCredentialsResponse credentialResponse = resources.getJerseyTest()
|
||||
.target("v1/archives/auth")
|
||||
@@ -385,7 +396,7 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getBackupInfo() throws VerificationFailedException {
|
||||
public void getBackupInfo() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||
BackupLevel.PAID, messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
@@ -405,13 +416,13 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void putMediaBatchSuccess() throws VerificationFailedException {
|
||||
public void putMediaBatchSuccess() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||
BackupLevel.PAID, messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
.thenReturn(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID));
|
||||
final byte[][] mediaIds = new byte[][]{TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)};
|
||||
when(backupManager.copyToBackup(any(), any()))
|
||||
when(backupManager.copyToBackup(any()))
|
||||
.thenReturn(Flux.just(
|
||||
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[0], 1),
|
||||
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[1], 1)));
|
||||
@@ -449,7 +460,7 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void putMediaBatchPartialFailure() throws VerificationFailedException {
|
||||
public void putMediaBatchPartialFailure() throws VerificationFailedException, BackupException {
|
||||
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||
BackupLevel.PAID, messagesBackupKey, aci);
|
||||
@@ -457,7 +468,7 @@ public class ArchiveControllerTest {
|
||||
.thenReturn(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID));
|
||||
|
||||
final byte[][] mediaIds = IntStream.range(0, 4).mapToObj(i -> TestRandomUtil.nextBytes(15)).toArray(byte[][]::new);
|
||||
when(backupManager.copyToBackup(any(), any()))
|
||||
when(backupManager.copyToBackup(any()))
|
||||
.thenReturn(Flux.just(
|
||||
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[0], 1),
|
||||
new CopyResult(CopyResult.Outcome.SOURCE_NOT_FOUND, mediaIds[1], null),
|
||||
@@ -508,7 +519,7 @@ public class ArchiveControllerTest {
|
||||
|
||||
|
||||
@Test
|
||||
public void copyMediaWithNegativeLength() throws VerificationFailedException {
|
||||
public void copyMediaWithNegativeLength() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||
BackupLevel.PAID, messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
@@ -541,7 +552,7 @@ public class ArchiveControllerTest {
|
||||
public void list(
|
||||
@CartesianTest.Values(booleans = {true, false}) final boolean cursorProvided,
|
||||
@CartesianTest.Values(booleans = {true, false}) final boolean cursorReturned)
|
||||
throws VerificationFailedException {
|
||||
throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||
BackupLevel.PAID, messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
@@ -576,7 +587,7 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void delete() throws VerificationFailedException {
|
||||
public void delete() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(BackupLevel.PAID,
|
||||
messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
@@ -612,7 +623,7 @@ public class ArchiveControllerTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
public void messagesUploadForm(Optional<Long> uploadLength, boolean expectSuccess) throws VerificationFailedException {
|
||||
public void messagesUploadForm(Optional<Long> uploadLength, boolean expectSuccess) throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation =
|
||||
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
@@ -641,7 +652,7 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mediaUploadForm() throws VerificationFailedException, RateLimitExceededException {
|
||||
public void mediaUploadForm() throws VerificationFailedException, BackupException, RateLimitExceededException {
|
||||
final BackupAuthCredentialPresentation presentation =
|
||||
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
@@ -671,7 +682,7 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readAuth() throws VerificationFailedException {
|
||||
public void readAuth() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation =
|
||||
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
@@ -689,7 +700,7 @@ public class ArchiveControllerTest {
|
||||
|
||||
|
||||
@Test
|
||||
public void svrbAuth() throws VerificationFailedException {
|
||||
public void svrbAuth() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation =
|
||||
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
@@ -706,7 +717,7 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readAuthInvalidParam() throws VerificationFailedException {
|
||||
public void readAuthInvalidParam() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation =
|
||||
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
||||
Response response = resources.getJerseyTest()
|
||||
@@ -728,7 +739,7 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteEntireBackup() throws VerificationFailedException {
|
||||
public void deleteEntireBackup() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation =
|
||||
backupAuthTestUtil.getPresentation(BackupLevel.PAID, messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
@@ -743,7 +754,7 @@ public class ArchiveControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invalidSourceAttachmentKey() throws VerificationFailedException {
|
||||
public void invalidSourceAttachmentKey() throws VerificationFailedException, BackupException {
|
||||
final BackupAuthCredentialPresentation presentation = backupAuthTestUtil.getPresentation(
|
||||
BackupLevel.PAID, messagesBackupKey, aci);
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
|
||||
@@ -8,7 +8,6 @@ package org.whispersystems.textsecuregcm.grpc;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -19,19 +18,16 @@ import io.grpc.StatusRuntimeException;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
import jakarta.ws.rs.client.WebTarget;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
@@ -62,10 +58,11 @@ import org.signal.libsignal.zkgroup.backups.BackupCredentialType;
|
||||
import org.signal.libsignal.zkgroup.backups.BackupLevel;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedBackupUser;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupAuthTestUtil;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupFailedZkAuthenticationException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupManager;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupUploadDescriptor;
|
||||
import org.whispersystems.textsecuregcm.backup.CopyResult;
|
||||
import org.whispersystems.textsecuregcm.controllers.ArchiveController;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.metrics.BackupMetrics;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
@@ -90,9 +87,12 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
when(backupManager.authenticateBackupUserAsync(any(), any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(
|
||||
backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID)));
|
||||
try {
|
||||
when(backupManager.authenticateBackupUser(any(), any(), any()))
|
||||
.thenReturn(backupUser(presentation.getBackupId(), BackupCredentialType.MESSAGES, BackupLevel.PAID));
|
||||
} catch (BackupFailedZkAuthenticationException e) {
|
||||
Assertions.fail(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -129,7 +129,7 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
@Test
|
||||
void putMediaBatchSuccess() {
|
||||
final byte[][] mediaIds = {TestRandomUtil.nextBytes(15), TestRandomUtil.nextBytes(15)};
|
||||
when(backupManager.copyToBackup(any(), any()))
|
||||
when(backupManager.copyToBackup(any()))
|
||||
.thenReturn(Flux.just(
|
||||
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[0], 1),
|
||||
new CopyResult(CopyResult.Outcome.SUCCESS, mediaIds[1], 1)));
|
||||
@@ -174,7 +174,7 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
CopyResult.Outcome.SOURCE_WRONG_LENGTH,
|
||||
CopyResult.Outcome.OUT_OF_QUOTA
|
||||
};
|
||||
when(backupManager.copyToBackup(any(), any()))
|
||||
when(backupManager.copyToBackup(any()))
|
||||
.thenReturn(Flux.fromStream(IntStream.range(0, 4)
|
||||
.mapToObj(i -> new CopyResult(
|
||||
outcomes[i],
|
||||
@@ -211,17 +211,17 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBackupInfo() {
|
||||
void getBackupInfo() throws BackupException {
|
||||
when(backupManager.backupInfo(any()))
|
||||
.thenReturn(new BackupManager.BackupInfo(1, "myBackupDir", "myMediaDir", "filename", Optional.empty()));
|
||||
|
||||
final GetBackupInfoResponse response = unauthenticatedServiceStub().getBackupInfo(GetBackupInfoRequest.newBuilder()
|
||||
.setSignedPresentation(signedPresentation(presentation))
|
||||
.build());
|
||||
assertThat(response.getBackupDir()).isEqualTo("myBackupDir");
|
||||
assertThat(response.getBackupName()).isEqualTo("filename");
|
||||
assertThat(response.getCdn()).isEqualTo(1);
|
||||
assertThat(response.getUsedSpace()).isEqualTo(0L);
|
||||
assertThat(response.getBackupInfo().getBackupDir()).isEqualTo("myBackupDir");
|
||||
assertThat(response.getBackupInfo().getBackupName()).isEqualTo("filename");
|
||||
assertThat(response.getBackupInfo().getCdn()).isEqualTo(1);
|
||||
assertThat(response.getBackupInfo().getUsedSpace()).isEqualTo(0L);
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
void list(
|
||||
@CartesianTest.Values(booleans = {true, false}) final boolean cursorProvided,
|
||||
@CartesianTest.Values(booleans = {true, false}) final boolean cursorReturned)
|
||||
throws VerificationFailedException {
|
||||
throws VerificationFailedException, BackupException {
|
||||
|
||||
final byte[] mediaId = TestRandomUtil.nextBytes(15);
|
||||
final Optional<String> expectedCursor = cursorProvided ? Optional.of("myCursor") : Optional.empty();
|
||||
@@ -250,15 +250,16 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
}
|
||||
|
||||
final ListMediaResponse response = unauthenticatedServiceStub().listMedia(request.build());
|
||||
assertThat(response.getPageCount()).isEqualTo(1);
|
||||
assertThat(response.getPage(0).getLength()).isEqualTo(100);
|
||||
assertThat(response.getPage(0).getMediaId().toByteArray()).isEqualTo(mediaId);
|
||||
assertThat(response.hasCursor() ? response.getCursor() : null).isEqualTo(returnedCursor.orElse(null));
|
||||
assertThat(response.getListResult().getPageCount()).isEqualTo(1);
|
||||
assertThat(response.getListResult().getPage(0).getLength()).isEqualTo(100);
|
||||
assertThat(response.getListResult().getPage(0).getMediaId().toByteArray()).isEqualTo(mediaId);
|
||||
assertThat(response.getListResult().hasCursor() ? response.getListResult().getCursor() : null)
|
||||
.isEqualTo(returnedCursor.orElse(null));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void delete() {
|
||||
void delete() throws BackupException {
|
||||
final DeleteMediaRequest request = DeleteMediaRequest.newBuilder()
|
||||
.setSignedPresentation(signedPresentation(presentation))
|
||||
.addAllItems(IntStream.range(0, 100).mapToObj(i ->
|
||||
@@ -278,7 +279,7 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
}
|
||||
|
||||
@Test
|
||||
void mediaUploadForm() throws RateLimitExceededException {
|
||||
void mediaUploadForm() throws RateLimitExceededException, BackupException {
|
||||
when(backupManager.createTemporaryAttachmentUploadDescriptor(any()))
|
||||
.thenReturn(new BackupUploadDescriptor(3, "abc", Map.of("k", "v"), "example.org"));
|
||||
final GetUploadFormRequest request = GetUploadFormRequest.newBuilder()
|
||||
@@ -287,10 +288,10 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
.build();
|
||||
|
||||
final GetUploadFormResponse uploadForm = unauthenticatedServiceStub().getUploadForm(request);
|
||||
assertThat(uploadForm.getCdn()).isEqualTo(3);
|
||||
assertThat(uploadForm.getKey()).isEqualTo("abc");
|
||||
assertThat(uploadForm.getHeadersMap()).containsExactlyEntriesOf(Map.of("k", "v"));
|
||||
assertThat(uploadForm.getSignedUploadLocation()).isEqualTo("example.org");
|
||||
assertThat(uploadForm.getUploadForm().getCdn()).isEqualTo(3);
|
||||
assertThat(uploadForm.getUploadForm().getKey()).isEqualTo("abc");
|
||||
assertThat(uploadForm.getUploadForm().getHeadersMap()).containsExactlyEntriesOf(Map.of("k", "v"));
|
||||
assertThat(uploadForm.getUploadForm().getSignedUploadLocation()).isEqualTo("example.org");
|
||||
|
||||
// rate limit
|
||||
Duration duration = Duration.ofSeconds(10);
|
||||
@@ -309,7 +310,7 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
public void messagesUploadForm(Optional<Long> uploadLength, boolean expectSuccess) {
|
||||
public void messagesUploadForm(Optional<Long> uploadLength, boolean allowedSize) throws BackupException {
|
||||
when(backupManager.createMessageBackupUploadDescriptor(any()))
|
||||
.thenReturn(new BackupUploadDescriptor(3, "abc", Map.of("k", "v"), "example.org"));
|
||||
final GetUploadFormRequest.MessagesUploadType.Builder builder = GetUploadFormRequest.MessagesUploadType.newBuilder();
|
||||
@@ -318,24 +319,20 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
.setMessages(builder.build())
|
||||
.setSignedPresentation(signedPresentation(presentation))
|
||||
.build();
|
||||
if (expectSuccess) {
|
||||
final GetUploadFormResponse uploadForm = unauthenticatedServiceStub().getUploadForm(request);
|
||||
assertThat(uploadForm.getCdn()).isEqualTo(3);
|
||||
assertThat(uploadForm.getKey()).isEqualTo("abc");
|
||||
assertThat(uploadForm.getHeadersMap()).containsExactlyEntriesOf(Map.of("k", "v"));
|
||||
assertThat(uploadForm.getSignedUploadLocation()).isEqualTo("example.org");
|
||||
final GetUploadFormResponse response = unauthenticatedServiceStub().getUploadForm(request);
|
||||
if (allowedSize) {
|
||||
assertThat(response.getUploadForm().getCdn()).isEqualTo(3);
|
||||
assertThat(response.getUploadForm().getKey()).isEqualTo("abc");
|
||||
assertThat(response.getUploadForm().getHeadersMap()).containsExactlyEntriesOf(Map.of("k", "v"));
|
||||
assertThat(response.getUploadForm().getSignedUploadLocation()).isEqualTo("example.org");
|
||||
} else {
|
||||
assertThatExceptionOfType(StatusRuntimeException.class)
|
||||
.isThrownBy(() -> unauthenticatedServiceStub().getUploadForm(request))
|
||||
.extracting(StatusRuntimeException::getStatus)
|
||||
.extracting(Status::getCode)
|
||||
.isEqualTo(Status.FAILED_PRECONDITION.getCode());
|
||||
assertThat(response.hasExceedsMaxUploadLength()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void readAuth() {
|
||||
void readAuth() throws BackupException {
|
||||
when(backupManager.generateReadAuth(any(), eq(3))).thenReturn(Map.of("key", "value"));
|
||||
|
||||
final GetCdnCredentialsResponse response = unauthenticatedServiceStub().getCdnCredentials(
|
||||
@@ -343,7 +340,7 @@ class BackupsAnonymousGrpcServiceTest extends
|
||||
.setCdn(3)
|
||||
.setSignedPresentation(signedPresentation(presentation))
|
||||
.build());
|
||||
assertThat(response.getHeadersMap()).containsExactlyEntriesOf(Map.of("key", "value"));
|
||||
assertThat(response.getCdnCredentials().getHeadersMap()).containsExactlyEntriesOf(Map.of("key", "value"));
|
||||
}
|
||||
|
||||
private static AuthenticatedBackupUser backupUser(final byte[] backupId, final BackupCredentialType credentialType,
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.signal.chat.backup.BackupsGrpc;
|
||||
import org.signal.chat.backup.GetBackupAuthCredentialsRequest;
|
||||
import org.signal.chat.backup.GetBackupAuthCredentialsResponse;
|
||||
import org.signal.chat.backup.RedeemReceiptRequest;
|
||||
import org.signal.chat.backup.RedeemReceiptResponse;
|
||||
import org.signal.chat.backup.SetBackupIdRequest;
|
||||
import org.signal.chat.common.ZkCredential;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
@@ -54,6 +55,12 @@ import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
|
||||
import org.whispersystems.textsecuregcm.auth.RedemptionRange;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupAuthManager;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupAuthTestUtil;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupBadReceiptException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupInvalidArgumentException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupMissingIdCommitmentException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupNotFoundException;
|
||||
import org.whispersystems.textsecuregcm.backup.BackupPermissionException;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.metrics.BackupMetrics;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
@@ -61,6 +68,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
import org.whispersystems.textsecuregcm.util.EnumMapUtil;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, BackupsGrpc.BackupsBlockingStub> {
|
||||
|
||||
@@ -98,7 +106,7 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
||||
|
||||
|
||||
@Test
|
||||
void setBackupId() throws RateLimitExceededException {
|
||||
void setBackupId() throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
|
||||
authenticatedServiceStub().setBackupId(
|
||||
SetBackupIdRequest.newBuilder()
|
||||
.setMediaBackupAuthCredentialRequest(ByteString.copyFrom(mediaAuthCredRequest.serialize()))
|
||||
@@ -111,7 +119,8 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void setBackupIdPartial(boolean media) throws RateLimitExceededException {
|
||||
void setBackupIdPartial(boolean media)
|
||||
throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
|
||||
final SetBackupIdRequest.Builder builder = SetBackupIdRequest.newBuilder();
|
||||
if (media) {
|
||||
builder.setMediaBackupAuthCredentialRequest(ByteString.copyFrom(mediaAuthCredRequest.serialize()));
|
||||
@@ -141,15 +150,14 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
||||
public static Stream<Arguments> setBackupIdException() {
|
||||
return Stream.of(
|
||||
Arguments.of(new RateLimitExceededException(null), Status.RESOURCE_EXHAUSTED),
|
||||
Arguments.of(Status.INVALID_ARGUMENT.withDescription("test").asRuntimeException(),
|
||||
Status.INVALID_ARGUMENT)
|
||||
);
|
||||
Arguments.of(new BackupPermissionException("test"), Status.INVALID_ARGUMENT),
|
||||
Arguments.of(new BackupInvalidArgumentException("test"), Status.INVALID_ARGUMENT));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void setBackupIdException(final Exception ex, final Status expected)
|
||||
throws RateLimitExceededException {
|
||||
throws RateLimitExceededException, BackupInvalidArgumentException, BackupPermissionException {
|
||||
doThrow(ex).when(backupAuthManager).commitBackupId(any(), any(), any(), any());
|
||||
|
||||
GrpcTestUtils.assertStatusException(
|
||||
@@ -160,8 +168,18 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void redeemReceipt() throws InvalidInputException, VerificationFailedException {
|
||||
public static Stream<Arguments> redeemReceipt() {
|
||||
return Stream.of(
|
||||
Arguments.of(null, RedeemReceiptResponse.OutcomeCase.SUCCESS),
|
||||
Arguments.of(new BackupBadReceiptException("test"), RedeemReceiptResponse.OutcomeCase.INVALID_RECEIPT),
|
||||
Arguments.of(new BackupMissingIdCommitmentException(), RedeemReceiptResponse.OutcomeCase.ACCOUNT_MISSING_COMMITMENT));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void redeemReceipt(@Nullable final BackupException exception, final RedeemReceiptResponse.OutcomeCase expectedOutcome)
|
||||
throws InvalidInputException, VerificationFailedException, BackupInvalidArgumentException, BackupMissingIdCommitmentException, BackupBadReceiptException {
|
||||
|
||||
final ServerSecretParams params = ServerSecretParams.generate();
|
||||
final ServerZkReceiptOperations serverOps = new ServerZkReceiptOperations(params);
|
||||
final ClientZkReceiptOperations clientOps = new ClientZkReceiptOperations(params.getPublicParams());
|
||||
@@ -171,16 +189,22 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
||||
final ReceiptCredential receiptCredential = clientOps.receiveReceiptCredential(rcrc, rcr);
|
||||
final ReceiptCredentialPresentation presentation = clientOps.createReceiptCredentialPresentation(receiptCredential);
|
||||
|
||||
authenticatedServiceStub().redeemReceipt(RedeemReceiptRequest.newBuilder()
|
||||
.setPresentation(ByteString.copyFrom(presentation.serialize()))
|
||||
.build());
|
||||
if (exception != null) {
|
||||
doThrow(exception).when(backupAuthManager).redeemReceipt(any(), any());
|
||||
}
|
||||
|
||||
final RedeemReceiptResponse redeemReceiptResponse = authenticatedServiceStub().redeemReceipt(
|
||||
RedeemReceiptRequest.newBuilder()
|
||||
.setPresentation(ByteString.copyFrom(presentation.serialize()))
|
||||
.build());
|
||||
assertThat(redeemReceiptResponse.getOutcomeCase()).isEqualTo(expectedOutcome);
|
||||
|
||||
verify(backupAuthManager).redeemReceipt(account, presentation);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void getCredentials() {
|
||||
void getCredentials() throws BackupNotFoundException {
|
||||
final Instant start = Instant.now().truncatedTo(ChronoUnit.DAYS);
|
||||
final Instant end = start.plus(Duration.ofDays(1));
|
||||
final RedemptionRange expectedRange = RedemptionRange.inclusive(Clock.systemUTC(), start, end);
|
||||
@@ -190,9 +214,12 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
||||
BackupLevel.PAID, backupAuthTestUtil.getRequest(messagesBackupKey, AUTHENTICATED_ACI), credentialType,
|
||||
start, end));
|
||||
|
||||
expectedCredentialsByType.forEach((credentialType, expectedCredentials) ->
|
||||
when(backupAuthManager.getBackupAuthCredentials(any(), eq(credentialType), eq(expectedRange)))
|
||||
.thenReturn(expectedCredentials));
|
||||
for (Map.Entry<BackupCredentialType, List<BackupAuthManager.Credential>> entry : expectedCredentialsByType.entrySet()) {
|
||||
final BackupCredentialType credentialType = entry.getKey();
|
||||
final List<BackupAuthManager.Credential> expectedCredentials = entry.getValue();
|
||||
when(backupAuthManager.getBackupAuthCredentials(any(), eq(credentialType), eq(expectedRange)))
|
||||
.thenReturn(expectedCredentials);
|
||||
}
|
||||
|
||||
final GetBackupAuthCredentialsResponse credentialResponse = authenticatedServiceStub().getBackupAuthCredentials(
|
||||
GetBackupAuthCredentialsRequest.newBuilder()
|
||||
@@ -202,8 +229,8 @@ class BackupsGrpcServiceTest extends SimpleBaseGrpcTest<BackupsGrpcService, Back
|
||||
expectedCredentialsByType.forEach((credentialType, expectedCredentials) -> {
|
||||
|
||||
final Map<Long, ZkCredential> creds = switch (credentialType) {
|
||||
case MESSAGES -> credentialResponse.getMessageCredentialsMap();
|
||||
case MEDIA -> credentialResponse.getMediaCredentialsMap();
|
||||
case MESSAGES -> credentialResponse.getCredentials().getMessageCredentialsMap();
|
||||
case MEDIA -> credentialResponse.getCredentials().getMediaCredentialsMap();
|
||||
};
|
||||
assertThat(creds).hasSize(expectedCredentials.size()).containsKey(start.getEpochSecond());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user