mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-19 19:18:03 +01: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();
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user