mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-19 20:18:05 +01:00
Allow rotating a single backup-id at a time
This commit is contained in:
@@ -17,7 +17,6 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import javax.annotation.Nullable;
|
||||
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
|
||||
@@ -101,40 +100,59 @@ public class BackupAuthManager {
|
||||
public CompletableFuture<Void> commitBackupId(
|
||||
final Account account,
|
||||
final Device device,
|
||||
final BackupAuthCredentialRequest messagesBackupCredentialRequest,
|
||||
final BackupAuthCredentialRequest mediaBackupCredentialRequest) {
|
||||
final Optional<BackupAuthCredentialRequest> messagesBackupCredentialRequest,
|
||||
final Optional<BackupAuthCredentialRequest> mediaBackupCredentialRequest) {
|
||||
if (!device.isPrimary()) {
|
||||
throw Status.PERMISSION_DENIED.withDescription("Only primary device can set backup-id").asRuntimeException();
|
||||
}
|
||||
final byte[] serializedMessageCredentialRequest = messagesBackupCredentialRequest.serialize();
|
||||
final byte[] serializedMediaCredentialRequest = mediaBackupCredentialRequest.serialize();
|
||||
|
||||
final boolean messageCredentialRequestMatches = account.getBackupCredentialRequest(BackupCredentialType.MESSAGES)
|
||||
.map(storedCredentialRequest -> MessageDigest.isEqual(storedCredentialRequest, serializedMessageCredentialRequest))
|
||||
.orElse(false);
|
||||
if (messagesBackupCredentialRequest.isEmpty() && mediaBackupCredentialRequest.isEmpty()) {
|
||||
throw Status.INVALID_ARGUMENT
|
||||
.withDescription("Must set at least one of message/media credential requests")
|
||||
.asRuntimeException();
|
||||
}
|
||||
|
||||
final boolean mediaCredentialRequestMatches = account.getBackupCredentialRequest(BackupCredentialType.MEDIA)
|
||||
.map(storedCredentialRequest -> MessageDigest.isEqual(storedCredentialRequest, serializedMediaCredentialRequest))
|
||||
.orElse(false);
|
||||
final byte[] storedMessageCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MESSAGES)
|
||||
.orElse(null);
|
||||
final byte[] storedMediaCredentialRequest = account.getBackupCredentialRequest(BackupCredentialType.MEDIA)
|
||||
.orElse(null);
|
||||
|
||||
if (messageCredentialRequestMatches && mediaCredentialRequestMatches) {
|
||||
// If the provided credential request is null, we want to set to the existing request
|
||||
final byte[] targetMessageCredentialRequest = messagesBackupCredentialRequest
|
||||
.map(BackupAuthCredentialRequest::serialize)
|
||||
.orElse(storedMessageCredentialRequest);
|
||||
final byte[] targetMediaCredentialRequest = mediaBackupCredentialRequest
|
||||
.map(BackupAuthCredentialRequest::serialize)
|
||||
.orElse(storedMediaCredentialRequest);
|
||||
|
||||
final boolean requiresMessageRotation =
|
||||
!MessageDigest.isEqual(targetMessageCredentialRequest, storedMessageCredentialRequest);
|
||||
final boolean requiresMediaRotation =
|
||||
!MessageDigest.isEqual(targetMediaCredentialRequest, storedMediaCredentialRequest);
|
||||
|
||||
if (!requiresMessageRotation && !requiresMediaRotation) {
|
||||
// No need to update or enforce rate limits, this is the credential that the user has already
|
||||
// committed to.
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
CompletionStage<Void> rateLimitFuture = rateLimiters
|
||||
.forDescriptor(RateLimiters.For.SET_BACKUP_ID)
|
||||
.validateAsync(account.getUuid());
|
||||
CompletableFuture<Void> rateLimitFuture = CompletableFuture.completedFuture(null);
|
||||
|
||||
if (!mediaCredentialRequestMatches && hasActiveVoucher(account)) {
|
||||
if (requiresMessageRotation) {
|
||||
rateLimitFuture = rateLimitFuture.thenCombine(
|
||||
rateLimiters.forDescriptor(RateLimiters.For.SET_BACKUP_ID).validateAsync(account.getUuid()),
|
||||
(_, _) -> null);
|
||||
}
|
||||
|
||||
if (requiresMediaRotation && hasActiveVoucher(account)) {
|
||||
rateLimitFuture = rateLimitFuture.thenCombine(
|
||||
rateLimiters.forDescriptor(RateLimiters.For.SET_PAID_MEDIA_BACKUP_ID).validateAsync(account.getUuid()),
|
||||
(ignore1, ignore2) -> null);
|
||||
(_, _) -> null);
|
||||
}
|
||||
|
||||
return rateLimitFuture.thenCompose(ignored -> this.accountsManager
|
||||
.updateAsync(account, a -> a.setBackupCredentialRequests(serializedMessageCredentialRequest, serializedMediaCredentialRequest))
|
||||
.updateAsync(account, a ->
|
||||
a.setBackupCredentialRequests(targetMessageCredentialRequest, targetMediaCredentialRequest))
|
||||
.thenRun(Util.NOOP))
|
||||
.toCompletableFuture();
|
||||
}
|
||||
|
||||
@@ -114,19 +114,20 @@ public class ArchiveController {
|
||||
@Schema(description = """
|
||||
A BackupAuthCredentialRequest containing a blinded encrypted backup-id, encoded in standard padded base64.
|
||||
This backup-id should be used for message backups only, and must have the message backup type set on the
|
||||
credential.
|
||||
credential. If absent, the message credential request will not be updated.
|
||||
""", implementation = String.class)
|
||||
@JsonDeserialize(using = BackupAuthCredentialAdapter.CredentialRequestDeserializer.class)
|
||||
@JsonSerialize(using = BackupAuthCredentialAdapter.CredentialRequestSerializer.class)
|
||||
@NotNull BackupAuthCredentialRequest messagesBackupAuthCredentialRequest,
|
||||
BackupAuthCredentialRequest messagesBackupAuthCredentialRequest,
|
||||
|
||||
@Schema(description = """
|
||||
A BackupAuthCredentialRequest containing a blinded encrypted backup-id, encoded in standard padded base64.
|
||||
This backup-id should be used for media only, and must have the media type set on the credential.
|
||||
This backup-id should be used for media only, and must have the media type set on the credential. If absent,
|
||||
only the media credential request will not be updated.
|
||||
""", implementation = String.class)
|
||||
@JsonDeserialize(using = BackupAuthCredentialAdapter.CredentialRequestDeserializer.class)
|
||||
@JsonSerialize(using = BackupAuthCredentialAdapter.CredentialRequestSerializer.class)
|
||||
@NotNull BackupAuthCredentialRequest mediaBackupAuthCredentialRequest) {}
|
||||
BackupAuthCredentialRequest mediaBackupAuthCredentialRequest) {}
|
||||
|
||||
|
||||
@PUT
|
||||
@@ -136,11 +137,13 @@ public class ArchiveController {
|
||||
@Operation(
|
||||
summary = "Set backup id",
|
||||
description = """
|
||||
Set a (blinded) backup-id for the account. Each account may have a single active backup-id that can be used
|
||||
to store and retrieve backups. Once the backup-id is set, BackupAuthCredentials can be generated
|
||||
using /v1/archives/auth.
|
||||
Set (blinded) backup-id(s) for the account. Each account may have a single active backup-id for each
|
||||
credential type that can be used to store and retrieve backups. Once the backup-id is set,
|
||||
BackupAuthCredentials can be generated using /v1/archives/auth.
|
||||
|
||||
The blinded backup-id and the key-pair used to blind it should be derived from a recoverable secret.
|
||||
|
||||
At least one of `messagesBackupAuthCredentialRequest`, `mediaBackupAuthCredentialRequest` must be set.
|
||||
""")
|
||||
@ApiResponse(responseCode = "204", description = "The backup-id was set")
|
||||
@ApiResponse(responseCode = "400", description = "The provided backup auth credential request was invalid")
|
||||
@@ -159,8 +162,9 @@ public class ArchiveController {
|
||||
.orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED));
|
||||
|
||||
return backupAuthManager
|
||||
.commitBackupId(account, device, setBackupIdRequest.messagesBackupAuthCredentialRequest,
|
||||
setBackupIdRequest.mediaBackupAuthCredentialRequest)
|
||||
.commitBackupId(account, device,
|
||||
Optional.ofNullable(setBackupIdRequest.messagesBackupAuthCredentialRequest),
|
||||
Optional.ofNullable(setBackupIdRequest.mediaBackupAuthCredentialRequest))
|
||||
.thenApply(Util.ASYNC_EMPTY_RESPONSE);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import io.micrometer.core.instrument.Tag;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import org.signal.chat.backup.GetBackupAuthCredentialsRequest;
|
||||
import org.signal.chat.backup.GetBackupAuthCredentialsResponse;
|
||||
@@ -46,17 +47,16 @@ public class BackupsGrpcService extends ReactorBackupsGrpc.BackupsImplBase {
|
||||
this.backupMetrics = backupMetrics;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<SetBackupIdResponse> setBackupId(SetBackupIdRequest request) {
|
||||
|
||||
final BackupAuthCredentialRequest messagesCredentialRequest = deserialize(
|
||||
final Optional<BackupAuthCredentialRequest> messagesCredentialRequest = deserializeWithEmptyPresenceCheck(
|
||||
BackupAuthCredentialRequest::new,
|
||||
request.getMessagesBackupAuthCredentialRequest().toByteArray());
|
||||
request.getMessagesBackupAuthCredentialRequest());
|
||||
|
||||
final BackupAuthCredentialRequest mediaCredentialRequest = deserialize(
|
||||
final Optional<BackupAuthCredentialRequest> mediaCredentialRequest = deserializeWithEmptyPresenceCheck(
|
||||
BackupAuthCredentialRequest::new,
|
||||
request.getMediaBackupAuthCredentialRequest().toByteArray());
|
||||
request.getMediaBackupAuthCredentialRequest());
|
||||
|
||||
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
|
||||
return authenticatedAccount()
|
||||
@@ -137,6 +137,13 @@ public class BackupsGrpcService extends ReactorBackupsGrpc.BackupsImplBase {
|
||||
T deserialize(byte[] bytes) throws InvalidInputException;
|
||||
}
|
||||
|
||||
private <T> Optional<T> deserializeWithEmptyPresenceCheck(Deserializer<T> deserializer, ByteString byteString) {
|
||||
if (byteString.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(deserialize(deserializer, byteString.toByteArray()));
|
||||
}
|
||||
|
||||
private <T> T deserialize(Deserializer<T> deserializer, byte[] bytes) {
|
||||
try {
|
||||
return deserializer.deserialize(bytes);
|
||||
|
||||
Reference in New Issue
Block a user