diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 6473a1d4b..6859b3e2a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -272,7 +272,6 @@ import org.whispersystems.textsecuregcm.workers.BackupUsageRecalculationCommand; import org.whispersystems.textsecuregcm.workers.CertificateCommand; import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand; import org.whispersystems.textsecuregcm.workers.DeleteUserCommand; -import org.whispersystems.textsecuregcm.workers.EncryptDeviceCreationTimestampCommand; import org.whispersystems.textsecuregcm.workers.IdleDeviceNotificationSchedulerFactory; import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand; import org.whispersystems.textsecuregcm.workers.NotifyIdleDevicesCommand; @@ -347,7 +346,6 @@ public class WhisperServerService extends Application accounts) { - final boolean isDryRun = getNamespace().getBoolean(DRY_RUN_ARGUMENT); - final int bufferSize = getNamespace().getInt(BUFFER_ARGUMENT); - - final Counter processedAccountCounter = - Metrics.counter(PROCESSED_ACCOUNT_COUNTER_NAME, "dryRun", String.valueOf(isDryRun)); - - accounts - // We've partially processed enough accounts now that this should speed up the crawler - .filter(a -> a.getDevices().stream().anyMatch(d -> d.getCreatedAtCiphertext() == null || d.getCreatedAtCiphertext().length == 0)) - .buffer(bufferSize) - .map(source -> { - final List shuffled = new ArrayList<>(source); - Collections.shuffle(shuffled); - return shuffled; - }) - .limitRate(2) - .flatMapIterable(Function.identity()) - .flatMap(account -> { - Mono encryptTimestampMono = isDryRun - ? Mono.empty() - : Mono.fromFuture( - () -> getCommandDependencies().accountsManager().updateAsync(account, a -> { - final IdentityKey aciIdentityKey = account.getIdentityKey(IdentityType.ACI); - for (final Device device : a.getDevices()) { - final byte[] createdAtCiphertext = EncryptDeviceCreationTimestampUtil.encrypt( - device.getCreated(), aciIdentityKey, - device.getId(), device.getRegistrationId(IdentityType.ACI)); - device.setCreatedAtCiphertext(createdAtCiphertext); - } - }).thenRun(Util.NOOP)); - return encryptTimestampMono - .doOnSuccess(_ -> processedAccountCounter.increment()) - .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)).maxBackoff(Duration.ofSeconds(4))) - .onErrorResume(throwable -> { - log.warn("Failed to encrypt creation timestamps on account {}", account.getUuid(), throwable); - return Mono.empty(); - }); - }, MAX_CONCURRENCY) - .then() - .block(); - - log.info("Finished encrypting device timestamps"); - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/workers/EncryptDeviceCreationTimestampCommandTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/workers/EncryptDeviceCreationTimestampCommandTest.java deleted file mode 100644 index 195e95b39..000000000 --- a/service/src/test/java/org/whispersystems/textsecuregcm/workers/EncryptDeviceCreationTimestampCommandTest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.whispersystems.textsecuregcm.workers; - -import net.sourceforge.argparse4j.inf.Namespace; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.signal.libsignal.protocol.IdentityKey; -import org.signal.libsignal.protocol.ecc.ECKeyPair; -import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; -import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.AccountsManager; -import org.whispersystems.textsecuregcm.storage.Device; -import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; -import org.whispersystems.textsecuregcm.tests.util.DevicesHelper; -import org.whispersystems.textsecuregcm.util.TestRandomUtil; -import reactor.core.publisher.Flux; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class EncryptDeviceCreationTimestampCommandTest { - - private static class TestEncryptDeviceCreationTimestampCommand extends EncryptDeviceCreationTimestampCommand { - - private final CommandDependencies commandDependencies; - private final Namespace namespace; - - public TestEncryptDeviceCreationTimestampCommand(final AccountsManager accountsManager, final boolean isDryRun) { - super(); - - commandDependencies = mock(CommandDependencies.class); - when(commandDependencies.accountsManager()).thenReturn(accountsManager); - - namespace = mock(Namespace.class); - when(namespace.getBoolean(EncryptDeviceCreationTimestampCommand.DRY_RUN_ARGUMENT)).thenReturn(isDryRun); - when(namespace.getInt(EncryptDeviceCreationTimestampCommand.BUFFER_ARGUMENT)).thenReturn(5); - } - - @Override - protected CommandDependencies getCommandDependencies() { - return commandDependencies; - } - - @Override - protected Namespace getNamespace() { - return namespace; - } - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void crawlAccounts(final boolean isDryRun) { - final String number = "+14152222222"; - final UUID uuid = UUID.randomUUID(); - - final Account testAccount = AccountsHelper.generateTestAccount(number, uuid, UUID.randomUUID(), List.of( - DevicesHelper.createDevice(Device.PRIMARY_ID)), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]); - testAccount.setIdentityKey(new IdentityKey(ECKeyPair.generate().getPublicKey())); - - final AccountsManager accountsManager = mock(AccountsManager.class); - when(accountsManager.updateAsync(any(), any())).thenAnswer(invocation -> { - final Account account = invocation.getArgument(0); - final Consumer updater = invocation.getArgument(1); - updater.accept(account); - return CompletableFuture.completedFuture(account); - }); - - final EncryptDeviceCreationTimestampCommand encryptDeviceCreationTimestampCommand = - new TestEncryptDeviceCreationTimestampCommand(accountsManager, isDryRun); - - encryptDeviceCreationTimestampCommand.crawlAccounts(Flux.just(testAccount)); - - if (isDryRun) { - verify(accountsManager, never()).updateAsync(any(), any()); - assertNull(testAccount.getDevices().getFirst().getCreatedAtCiphertext()); - } else { - verify(accountsManager).updateAsync(eq(testAccount), any()); - assertNotNull(testAccount.getDevices().getFirst().getCreatedAtCiphertext()); - } - } - - @Test - void crawlAccountsWithEncryptedTimestamps() { - final Account unencryptedTimestampAccount = AccountsHelper.generateTestAccount("+14152222222", UUID.randomUUID(), UUID.randomUUID(), List.of( - DevicesHelper.createDevice(Device.PRIMARY_ID)), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]); - unencryptedTimestampAccount.setIdentityKey(new IdentityKey(ECKeyPair.generate().getPublicKey())); - - final Account encryptedTimestampAccount = AccountsHelper.generateTestAccount("+14152222223", UUID.randomUUID(), UUID.randomUUID(), List.of( - DevicesHelper.createDevice(Device.PRIMARY_ID)), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]); - encryptedTimestampAccount.setIdentityKey(new IdentityKey(ECKeyPair.generate().getPublicKey())); - encryptedTimestampAccount.getDevices().getFirst().setCreatedAtCiphertext(TestRandomUtil.nextBytes(56)); - - final Account halfEncryptedTimestampAccount = AccountsHelper.generateTestAccount("+14152222224", UUID.randomUUID(), UUID.randomUUID(), List.of( - DevicesHelper.createDevice(Device.PRIMARY_ID), DevicesHelper.createDevice((byte) 2)), new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH]); - halfEncryptedTimestampAccount.setIdentityKey(new IdentityKey(ECKeyPair.generate().getPublicKey())); - halfEncryptedTimestampAccount.getDevices().getFirst().setCreatedAtCiphertext(TestRandomUtil.nextBytes(56)); - - final AccountsManager accountsManager = mock(AccountsManager.class); - when(accountsManager.updateAsync(any(), any())).thenAnswer(invocation -> { - final Account account = invocation.getArgument(0); - final Consumer updater = invocation.getArgument(1); - updater.accept(account); - return CompletableFuture.completedFuture(account); - }); - - final EncryptDeviceCreationTimestampCommand encryptDeviceCreationTimestampCommand = - new TestEncryptDeviceCreationTimestampCommand(accountsManager, false); - - assertNull(unencryptedTimestampAccount.getDevices().getFirst().getCreatedAtCiphertext()); - assertNull(halfEncryptedTimestampAccount.getDevices().get(1).getCreatedAtCiphertext()); - - encryptDeviceCreationTimestampCommand.crawlAccounts(Flux.just(unencryptedTimestampAccount, - encryptedTimestampAccount, - halfEncryptedTimestampAccount)); - - verify(accountsManager, times(1)).updateAsync(eq(unencryptedTimestampAccount), any()); - verify(accountsManager, never()).updateAsync(eq(encryptedTimestampAccount), any()); - verify(accountsManager, times(1)).updateAsync(eq(halfEncryptedTimestampAccount), any()); - - assertNotNull(unencryptedTimestampAccount.getDevices().getFirst().getCreatedAtCiphertext()); - assertNotNull(halfEncryptedTimestampAccount.getDevices().get(1).getCreatedAtCiphertext()); - } -}