Skip to content

Commit

Permalink
Update to libsignal 0.45 and use libsignal's BackupLevel
Browse files Browse the repository at this point in the history
  • Loading branch information
ravi-signal committed Apr 25, 2024
1 parent c8efcf5 commit 19944bf
Show file tree
Hide file tree
Showing 15 changed files with 257 additions and 278 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@
<dependency>
<groupId>org.signal</groupId>
<artifactId>libsignal-server</artifactId>
<version>0.44.0</version>
<version>0.45.0</version>
</dependency>
<dependency>
<groupId>org.signal.forks</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@

package org.whispersystems.textsecuregcm.auth;

import org.whispersystems.textsecuregcm.backup.BackupTier;
import org.signal.libsignal.zkgroup.backups.BackupLevel;

public record AuthenticatedBackupUser(byte[] backupId, BackupTier backupTier, String backupDir, String mediaDir) {}
public record AuthenticatedBackupUser(byte[] backupId, BackupLevel backupLevel, String backupDir, String mediaDir) {}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
Expand All @@ -21,6 +20,7 @@
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialRequest;
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialResponse;
import org.signal.libsignal.zkgroup.backups.BackupLevel;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
import org.signal.libsignal.zkgroup.receipts.ReceiptSerial;
import org.signal.libsignal.zkgroup.receipts.ServerZkReceiptOperations;
Expand Down Expand Up @@ -89,7 +89,7 @@ public BackupAuthManager(
*/
public CompletableFuture<Void> commitBackupId(final Account account,
final BackupAuthCredentialRequest backupAuthCredentialRequest) throws RateLimitExceededException {
if (configuredReceiptLevel(account).isEmpty()) {
if (configuredBackupLevel(account).isEmpty()) {
throw Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException();
}

Expand Down Expand Up @@ -141,7 +141,7 @@ public CompletableFuture<List<Credential>> getBackupAuthCredentials(
}

// If this account isn't allowed some level of backup access via configuration, don't continue
final long configuredReceiptLevel = configuredReceiptLevel(account).orElseThrow(() ->
final BackupLevel configuredBackupLevel = configuredBackupLevel(account).orElseThrow(() ->
Status.PERMISSION_DENIED.withDescription("Backups not allowed on account").asRuntimeException());

final Instant startOfDay = clock.instant().truncatedTo(ChronoUnit.DAYS);
Expand Down Expand Up @@ -169,9 +169,9 @@ public CompletableFuture<List<Credential>> getBackupAuthCredentials(
.map(redemptionTime -> {
// Check if the account has a voucher that's good for a certain receiptLevel at redemption time, otherwise
// use the default receipt level
final long receiptLevel = storedReceiptLevel(account, redemptionTime).orElse(configuredReceiptLevel);
final BackupLevel backupLevel = storedBackupLevel(account, redemptionTime).orElse(configuredBackupLevel);
return new Credential(
credentialReq.issueCredential(redemptionTime, receiptLevel, serverSecretParams),
credentialReq.issueCredential(redemptionTime, backupLevel, serverSecretParams),
redemptionTime);
})
.toList());
Expand Down Expand Up @@ -208,10 +208,11 @@ public CompletableFuture<Void> redeemReceipt(

final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();

BackupTier.fromReceiptLevel(receiptLevel).filter(BackupTier.MEDIA::equals)
.orElseThrow(() -> Status.INVALID_ARGUMENT
.withDescription("server does not recognize the requested receipt level")
.asRuntimeException());
if (BackupLevelUtil.fromReceiptLevel(receiptLevel) != BackupLevel.MEDIA) {
throw Status.INVALID_ARGUMENT
.withDescription("server does not recognize the requested receipt level")
.asRuntimeException();
}

return redeemedReceiptsManager
.put(receiptSerial, receiptExpiration.getEpochSecond(), receiptLevel, account.getUuid())
Expand Down Expand Up @@ -262,10 +263,11 @@ private boolean hasExpiredVoucher(final Account account) {
* @param redemptionTime The time to check against the expiration time
* @return The receipt level on the backup voucher, or empty if the account does not have one or it is expired
*/
private Optional<Long> storedReceiptLevel(final Account account, final Instant redemptionTime) {
private Optional<BackupLevel> storedBackupLevel(final Account account, final Instant redemptionTime) {
return Optional.ofNullable(account.getBackupVoucher())
.filter(backupVoucher -> !redemptionTime.isAfter(backupVoucher.expiration()))
.map(Account.BackupVoucher::receiptLevel);
.map(Account.BackupVoucher::receiptLevel)
.map(BackupLevelUtil::fromReceiptLevel);
}

/**
Expand All @@ -275,12 +277,12 @@ private Optional<Long> storedReceiptLevel(final Account account, final Instant r
* @return If present, the default receipt level that should be used for the account if the account does not have a
* BackupVoucher. Empty if the account should never have backup access
*/
private Optional<Long> configuredReceiptLevel(final Account account) {
private Optional<BackupLevel> configuredBackupLevel(final Account account) {
if (inExperiment(BACKUP_MEDIA_EXPERIMENT_NAME, account)) {
return Optional.of(BackupTier.MEDIA.getReceiptLevel());
return Optional.of(BackupLevel.MEDIA);
}
if (inExperiment(BACKUP_EXPERIMENT_NAME, account)) {
return Optional.of(BackupTier.MESSAGES.getReceiptLevel());
return Optional.of(BackupLevel.MESSAGES);
}
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.backup;

import org.signal.libsignal.zkgroup.backups.BackupLevel;

public class BackupLevelUtil {
public static BackupLevel fromReceiptLevel(long receiptLevel) {
try {
return BackupLevel.fromValue(Math.toIntExact(receiptLevel));
} catch (ArithmeticException e) {
throw new IllegalArgumentException("Invalid receipt level: " + receiptLevel);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.backups.BackupAuthCredentialPresentation;
import org.signal.libsignal.zkgroup.backups.BackupLevel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator;
Expand Down Expand Up @@ -126,14 +127,8 @@ public CompletableFuture<Void> setPublicKey(
// Note: this is a special case where we can't validate the presentation signature against the stored public key
// because we are currently setting it. We check against the provided public key, but we must also verify that
// there isn't an existing, different stored public key for the backup-id (verified with a condition expression)
final BackupTier backupTier = verifyPresentation(presentation).verifySignature(signature, publicKey);
if (backupTier.compareTo(BackupTier.MESSAGES) < 0) {
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
throw Status.PERMISSION_DENIED
.withDescription("credential does not support setting public key")
.asRuntimeException();
}
return backupsDb.setPublicKey(presentation.getBackupId(), backupTier, publicKey)
final BackupLevel backupLevel = verifyPresentation(presentation).verifySignature(signature, publicKey);
return backupsDb.setPublicKey(presentation.getBackupId(), backupLevel, publicKey)
.exceptionally(ExceptionUtils.exceptionallyHandler(PublicKeyConflictException.class, ex -> {
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
SUCCESS_TAG_NAME, String.valueOf(false),
Expand All @@ -156,7 +151,7 @@ public CompletableFuture<Void> setPublicKey(
*/
public CompletableFuture<BackupUploadDescriptor> createMessageBackupUploadDescriptor(
final AuthenticatedBackupUser backupUser) {
checkBackupTier(backupUser, BackupTier.MESSAGES);
checkBackupLevel(backupUser, BackupLevel.MESSAGES);

// this could race with concurrent updates, but the only effect would be last-writer-wins on the timestamp
return backupsDb
Expand All @@ -166,7 +161,7 @@ public CompletableFuture<BackupUploadDescriptor> createMessageBackupUploadDescri

public BackupUploadDescriptor createTemporaryAttachmentUploadDescriptor(final AuthenticatedBackupUser backupUser)
throws RateLimitExceededException {
checkBackupTier(backupUser, BackupTier.MEDIA);
checkBackupLevel(backupUser, BackupLevel.MEDIA);

RateLimiter.adaptLegacyException(() -> rateLimiters
.forDescriptor(RateLimiters.For.BACKUP_ATTACHMENT)
Expand All @@ -185,7 +180,7 @@ public BackupUploadDescriptor createTemporaryAttachmentUploadDescriptor(final Au
* @param backupUser an already ZK authenticated backup user
*/
public CompletableFuture<Void> ttlRefresh(final AuthenticatedBackupUser backupUser) {
checkBackupTier(backupUser, BackupTier.MESSAGES);
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
// update message backup TTL
return backupsDb.ttlRefresh(backupUser);
}
Expand All @@ -200,7 +195,7 @@ public record BackupInfo(int cdn, String backupSubdir, String mediaSubdir, Strin
* @return Information about the existing backup
*/
public CompletableFuture<BackupInfo> backupInfo(final AuthenticatedBackupUser backupUser) {
checkBackupTier(backupUser, BackupTier.MESSAGES);
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
return backupsDb.describeBackup(backupUser)
.thenApply(backupDescription -> new BackupInfo(
backupDescription.cdn(),
Expand All @@ -218,7 +213,7 @@ public CompletableFuture<BackupInfo> backupInfo(final AuthenticatedBackupUser ba
* @return true if mediaLength bytes can be stored
*/
public CompletableFuture<Boolean> canStoreMedia(final AuthenticatedBackupUser backupUser, final long mediaLength) {
checkBackupTier(backupUser, BackupTier.MEDIA);
checkBackupLevel(backupUser, BackupLevel.MEDIA);
return backupsDb.getMediaUsage(backupUser)
.thenComposeAsync(info -> {
final boolean canStore = MAX_TOTAL_BACKUP_MEDIA_BYTES - info.usageInfo().bytesUsed() >= mediaLength;
Expand Down Expand Up @@ -269,7 +264,7 @@ public CompletableFuture<StorageDescriptor> copyToBackup(
final int sourceLength,
final MediaEncryptionParameters encryptionParameters,
final byte[] destinationMediaId) {
checkBackupTier(backupUser, BackupTier.MEDIA);
checkBackupLevel(backupUser, BackupLevel.MEDIA);
if (sourceLength > MAX_MEDIA_OBJECT_SIZE) {
throw Status.INVALID_ARGUMENT
.withDescription("Invalid sourceObject size")
Expand Down Expand Up @@ -331,7 +326,7 @@ private URI attachmentReadUri(final int cdn, final String key) throws IOExceptio
* @return A map of headers to include with CDN requests
*/
public Map<String, String> generateReadAuth(final AuthenticatedBackupUser backupUser, final int cdnNumber) {
checkBackupTier(backupUser, BackupTier.MESSAGES);
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
if (cdnNumber != 3) {
throw Status.INVALID_ARGUMENT.withDescription("unknown cdn").asRuntimeException();
}
Expand Down Expand Up @@ -359,7 +354,7 @@ public CompletionStage<ListMediaResult> list(
final AuthenticatedBackupUser backupUser,
final Optional<String> cursor,
final int limit) {
checkBackupTier(backupUser, BackupTier.MESSAGES);
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
return remoteStorageManager.list(cdnMediaDirectory(backupUser), cursor, limit)
.thenApply(result ->
new ListMediaResult(
Expand All @@ -377,7 +372,7 @@ public CompletionStage<ListMediaResult> list(
}

public CompletableFuture<Void> deleteEntireBackup(final AuthenticatedBackupUser backupUser) {
checkBackupTier(backupUser, BackupTier.MESSAGES);
checkBackupLevel(backupUser, BackupLevel.MESSAGES);
return backupsDb
// Try to swap out the backupDir for the user
.scheduleBackupDeletion(backupUser)
Expand All @@ -395,7 +390,7 @@ private record DeleteFailure(Throwable e) implements Either {}

public CompletableFuture<Void> delete(final AuthenticatedBackupUser backupUser,
final List<StorageDescriptor> storageDescriptors) {
checkBackupTier(backupUser, BackupTier.MESSAGES);
checkBackupLevel(backupUser, BackupLevel.MESSAGES);

if (storageDescriptors.stream().anyMatch(sd -> sd.cdn() != remoteStorageManager.cdnNumber())) {
throw Status.INVALID_ARGUMENT
Expand Down Expand Up @@ -556,7 +551,7 @@ private CompletableFuture<Void> deletePrefix(final String prefixToDelete, int co

interface PresentationSignatureVerifier {

BackupTier verifySignature(byte[] signature, ECPublicKey publicKey);
BackupLevel verifySignature(byte[] signature, ECPublicKey publicKey);
}

/**
Expand Down Expand Up @@ -588,27 +583,19 @@ private PresentationSignatureVerifier verifyPresentation(final BackupAuthCredent
.withDescription("backup auth credential presentation signature verification failed")
.asRuntimeException();
}
return BackupTier
.fromReceiptLevel(presentation.getReceiptLevel())
.orElseThrow(() -> {
Metrics.counter(ZK_AUTHN_COUNTER_NAME,
SUCCESS_TAG_NAME, String.valueOf(false),
FAILURE_REASON_TAG_NAME, "invalid_receipt_level")
.increment();
return Status.PERMISSION_DENIED.withDescription("invalid receipt level").asRuntimeException();
});
return presentation.getBackupLevel();
};
}

/**
* Check that the authenticated backup user is authorized to use the provided backupTier
* Check that the authenticated backup user is authorized to use the provided backupLevel
*
* @param backupUser The backup user to check
* @param backupTier The authorization level to verify the backupUser has access to
* @throws {@link Status#PERMISSION_DENIED} error if the backup user is not authorized to access {@code backupTier}
* @param backupLevel The authorization level to verify the backupUser has access to
* @throws {@link Status#PERMISSION_DENIED} error if the backup user is not authorized to access {@code backupLevel}
*/
private static void checkBackupTier(final AuthenticatedBackupUser backupUser, final BackupTier backupTier) {
if (backupUser.backupTier().compareTo(backupTier) < 0) {
private static void checkBackupLevel(final AuthenticatedBackupUser backupUser, final BackupLevel backupLevel) {
if (backupUser.backupLevel().compareTo(backupLevel) < 0) {
Metrics.counter(ZK_AUTHZ_FAILURE_COUNTER_NAME).increment();
throw Status.PERMISSION_DENIED
.withDescription("credential does not support the requested operation")
Expand Down

This file was deleted.

Loading

0 comments on commit 19944bf

Please sign in to comment.