Add a command to copy signed pre-keys from `Account` records to their own table
This commit is contained in:
parent
a3e82dfae8
commit
2d154eb0cf
|
@ -198,6 +198,7 @@ import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand
|
||||||
import org.whispersystems.textsecuregcm.workers.CrawlAccountsCommand;
|
import org.whispersystems.textsecuregcm.workers.CrawlAccountsCommand;
|
||||||
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
|
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
|
||||||
import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand;
|
import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand;
|
||||||
|
import org.whispersystems.textsecuregcm.workers.MigrateSignedECPreKeysCommand;
|
||||||
import org.whispersystems.textsecuregcm.workers.ScheduledApnPushNotificationSenderServiceCommand;
|
import org.whispersystems.textsecuregcm.workers.ScheduledApnPushNotificationSenderServiceCommand;
|
||||||
import org.whispersystems.textsecuregcm.workers.ServerVersionCommand;
|
import org.whispersystems.textsecuregcm.workers.ServerVersionCommand;
|
||||||
import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask;
|
import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask;
|
||||||
|
@ -252,6 +253,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
bootstrap.addCommand(new CrawlAccountsCommand());
|
bootstrap.addCommand(new CrawlAccountsCommand());
|
||||||
bootstrap.addCommand(new ScheduledApnPushNotificationSenderServiceCommand());
|
bootstrap.addCommand(new ScheduledApnPushNotificationSenderServiceCommand());
|
||||||
bootstrap.addCommand(new MessagePersisterServiceCommand());
|
bootstrap.addCommand(new MessagePersisterServiceCommand());
|
||||||
|
bootstrap.addCommand(new MigrateSignedECPreKeysCommand());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -318,7 +320,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||||
config.getDynamoDbTables().getEcKeys().getTableName(),
|
config.getDynamoDbTables().getEcKeys().getTableName(),
|
||||||
config.getDynamoDbTables().getEcSignedPreKeys().getTableName(),
|
config.getDynamoDbTables().getEcSignedPreKeys().getTableName(),
|
||||||
config.getDynamoDbTables().getKemKeys().getTableName(),
|
config.getDynamoDbTables().getKemKeys().getTableName(),
|
||||||
config.getDynamoDbTables().getKemLastResortKeys().getTableName());
|
config.getDynamoDbTables().getKemLastResortKeys().getTableName(),
|
||||||
|
dynamicConfigurationManager);
|
||||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient, dynamoDbAsyncClient,
|
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient, dynamoDbAsyncClient,
|
||||||
config.getDynamoDbTables().getMessages().getTableName(),
|
config.getDynamoDbTables().getMessages().getTableName(),
|
||||||
config.getDynamoDbTables().getMessages().getExpiration(),
|
config.getDynamoDbTables().getMessages().getExpiration(),
|
||||||
|
|
|
@ -55,6 +55,10 @@ public class DynamicConfiguration {
|
||||||
@Valid
|
@Valid
|
||||||
DynamicRateLimitPolicy rateLimitPolicy = new DynamicRateLimitPolicy(false);
|
DynamicRateLimitPolicy rateLimitPolicy = new DynamicRateLimitPolicy(false);
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
@Valid
|
||||||
|
DynamicECPreKeyMigrationConfiguration ecPreKeyMigration = new DynamicECPreKeyMigrationConfiguration(false);
|
||||||
|
|
||||||
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
|
public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
|
||||||
final String experimentName) {
|
final String experimentName) {
|
||||||
return Optional.ofNullable(experiments.get(experimentName));
|
return Optional.ofNullable(experiments.get(experimentName));
|
||||||
|
@ -97,4 +101,7 @@ public class DynamicConfiguration {
|
||||||
return rateLimitPolicy;
|
return rateLimitPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DynamicECPreKeyMigrationConfiguration getEcPreKeyMigrationConfiguration() {
|
||||||
|
return ecPreKeyMigration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.configuration.dynamic;
|
||||||
|
|
||||||
|
public record DynamicECPreKeyMigrationConfiguration(boolean storeEcSignedPreKeys) {
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||||
|
@ -20,6 +21,8 @@ import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||||
|
|
||||||
public class KeysManager {
|
public class KeysManager {
|
||||||
|
|
||||||
|
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||||
|
|
||||||
private final SingleUseECPreKeyStore ecPreKeys;
|
private final SingleUseECPreKeyStore ecPreKeys;
|
||||||
private final SingleUseKEMPreKeyStore pqPreKeys;
|
private final SingleUseKEMPreKeyStore pqPreKeys;
|
||||||
private final RepeatedUseECSignedPreKeyStore ecSignedPreKeys;
|
private final RepeatedUseECSignedPreKeyStore ecSignedPreKeys;
|
||||||
|
@ -30,11 +33,13 @@ public class KeysManager {
|
||||||
final String ecTableName,
|
final String ecTableName,
|
||||||
final String pqTableName,
|
final String pqTableName,
|
||||||
final String ecSignedPreKeysTableName,
|
final String ecSignedPreKeysTableName,
|
||||||
final String pqLastResortTableName) {
|
final String pqLastResortTableName,
|
||||||
|
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
|
||||||
this.ecPreKeys = new SingleUseECPreKeyStore(dynamoDbAsyncClient, ecTableName);
|
this.ecPreKeys = new SingleUseECPreKeyStore(dynamoDbAsyncClient, ecTableName);
|
||||||
this.pqPreKeys = new SingleUseKEMPreKeyStore(dynamoDbAsyncClient, pqTableName);
|
this.pqPreKeys = new SingleUseKEMPreKeyStore(dynamoDbAsyncClient, pqTableName);
|
||||||
this.ecSignedPreKeys = new RepeatedUseECSignedPreKeyStore(dynamoDbAsyncClient, ecSignedPreKeysTableName);
|
this.ecSignedPreKeys = new RepeatedUseECSignedPreKeyStore(dynamoDbAsyncClient, ecSignedPreKeysTableName);
|
||||||
this.pqLastResortKeys = new RepeatedUseKEMSignedPreKeyStore(dynamoDbAsyncClient, pqLastResortTableName);
|
this.pqLastResortKeys = new RepeatedUseKEMSignedPreKeyStore(dynamoDbAsyncClient, pqLastResortTableName);
|
||||||
|
this.dynamicConfigurationManager = dynamicConfigurationManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void store(final UUID identifier, final long deviceId, final List<ECPreKey> keys) {
|
public void store(final UUID identifier, final long deviceId, final List<ECPreKey> keys) {
|
||||||
|
@ -58,7 +63,7 @@ public class KeysManager {
|
||||||
storeFutures.add(pqPreKeys.store(identifier, deviceId, pqKeys));
|
storeFutures.add(pqPreKeys.store(identifier, deviceId, pqKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ecSignedPreKey != null) {
|
if (ecSignedPreKey != null && dynamicConfigurationManager.getConfiguration().getEcPreKeyMigrationConfiguration().storeEcSignedPreKeys()) {
|
||||||
storeFutures.add(ecSignedPreKeys.store(identifier, deviceId, ecSignedPreKey));
|
storeFutures.add(ecSignedPreKeys.store(identifier, deviceId, ecSignedPreKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +75,13 @@ public class KeysManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void storeEcSignedPreKeys(final UUID identifier, final Map<Long, ECSignedPreKey> keys) {
|
public void storeEcSignedPreKeys(final UUID identifier, final Map<Long, ECSignedPreKey> keys) {
|
||||||
ecSignedPreKeys.store(identifier, keys).join();
|
if (dynamicConfigurationManager.getConfiguration().getEcPreKeyMigrationConfiguration().storeEcSignedPreKeys()) {
|
||||||
|
ecSignedPreKeys.store(identifier, keys).join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Boolean> storeEcSignedPreKeyIfAbsent(final UUID identifier, final long deviceId, final ECSignedPreKey signedPreKey) {
|
||||||
|
return ecSignedPreKeys.storeIfAbsent(identifier, deviceId, signedPreKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void storePqLastResort(final UUID identifier, final Map<Long, KEMSignedPreKey> keys) {
|
public void storePqLastResort(final UUID identifier, final Map<Long, KEMSignedPreKey> keys) {
|
||||||
|
|
|
@ -167,7 +167,8 @@ public class AssignUsernameCommand extends EnvironmentCommand<WhisperServerConfi
|
||||||
configuration.getDynamoDbTables().getEcKeys().getTableName(),
|
configuration.getDynamoDbTables().getEcKeys().getTableName(),
|
||||||
configuration.getDynamoDbTables().getEcSignedPreKeys().getTableName(),
|
configuration.getDynamoDbTables().getEcSignedPreKeys().getTableName(),
|
||||||
configuration.getDynamoDbTables().getKemKeys().getTableName(),
|
configuration.getDynamoDbTables().getKemKeys().getTableName(),
|
||||||
configuration.getDynamoDbTables().getKemLastResortKeys().getTableName());
|
configuration.getDynamoDbTables().getKemLastResortKeys().getTableName(),
|
||||||
|
dynamicConfigurationManager);
|
||||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient, dynamoDbAsyncClient,
|
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient, dynamoDbAsyncClient,
|
||||||
configuration.getDynamoDbTables().getMessages().getTableName(),
|
configuration.getDynamoDbTables().getMessages().getTableName(),
|
||||||
configuration.getDynamoDbTables().getMessages().getExpiration(),
|
configuration.getDynamoDbTables().getMessages().getExpiration(),
|
||||||
|
|
|
@ -151,7 +151,8 @@ record CommandDependencies(
|
||||||
configuration.getDynamoDbTables().getEcKeys().getTableName(),
|
configuration.getDynamoDbTables().getEcKeys().getTableName(),
|
||||||
configuration.getDynamoDbTables().getEcSignedPreKeys().getTableName(),
|
configuration.getDynamoDbTables().getEcSignedPreKeys().getTableName(),
|
||||||
configuration.getDynamoDbTables().getKemKeys().getTableName(),
|
configuration.getDynamoDbTables().getKemKeys().getTableName(),
|
||||||
configuration.getDynamoDbTables().getKemLastResortKeys().getTableName());
|
configuration.getDynamoDbTables().getKemLastResortKeys().getTableName(),
|
||||||
|
dynamicConfigurationManager);
|
||||||
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient, dynamoDbAsyncClient,
|
MessagesDynamoDb messagesDynamoDb = new MessagesDynamoDb(dynamoDbClient, dynamoDbAsyncClient,
|
||||||
configuration.getDynamoDbTables().getMessages().getTableName(),
|
configuration.getDynamoDbTables().getMessages().getTableName(),
|
||||||
configuration.getDynamoDbTables().getMessages().getExpiration(),
|
configuration.getDynamoDbTables().getMessages().getExpiration(),
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.whispersystems.textsecuregcm.workers;
|
||||||
|
|
||||||
|
import io.micrometer.core.instrument.Metrics;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||||
|
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Account;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.KeysManager;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.util.function.Tuple3;
|
||||||
|
import reactor.util.function.Tuples;
|
||||||
|
|
||||||
|
public class MigrateSignedECPreKeysCommand extends AbstractSinglePassCrawlAccountsCommand {
|
||||||
|
|
||||||
|
private static final String STORE_KEY_ATTEMPT_COUNTER_NAME =
|
||||||
|
MetricsUtil.name(MigrateSignedECPreKeysCommand.class, "storeKeyAttempt");
|
||||||
|
|
||||||
|
public MigrateSignedECPreKeysCommand() {
|
||||||
|
super("migrate-signed-ec-pre-keys", "Migrate signed EC pre-keys from Account records to a dedicated table");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void crawlAccounts(final Flux<Account> accounts) {
|
||||||
|
final KeysManager keysManager = getCommandDependencies().keysManager();
|
||||||
|
|
||||||
|
accounts.flatMap(account -> Flux.fromIterable(account.getDevices())
|
||||||
|
.flatMap(device -> {
|
||||||
|
final List<Tuple3<UUID, Long, ECSignedPreKey>> keys = new ArrayList<>(2);
|
||||||
|
|
||||||
|
if (device.getSignedPreKey() != null) {
|
||||||
|
keys.add(Tuples.of(account.getUuid(), device.getId(), device.getSignedPreKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.getPhoneNumberIdentitySignedPreKey() != null) {
|
||||||
|
keys.add(Tuples.of(account.getPhoneNumberIdentifier(), device.getId(), device.getPhoneNumberIdentitySignedPreKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Flux.fromIterable(keys);
|
||||||
|
}))
|
||||||
|
.flatMap(keyTuple -> Mono.fromFuture(
|
||||||
|
keysManager.storeEcSignedPreKeyIfAbsent(keyTuple.getT1(), keyTuple.getT2(), keyTuple.getT3())))
|
||||||
|
.doOnNext(keyStored -> Metrics.counter(STORE_KEY_ATTEMPT_COUNTER_NAME, "stored", String.valueOf(keyStored)).increment())
|
||||||
|
.blockLast();
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
|
import static org.junit.jupiter.api.Assertions.assertIterableEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -20,6 +22,8 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
import org.signal.libsignal.protocol.ecc.Curve;
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||||
|
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicECPreKeyMigrationConfiguration;
|
||||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||||
|
@ -28,6 +32,7 @@ import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
||||||
|
|
||||||
class KeysManagerTest {
|
class KeysManagerTest {
|
||||||
|
|
||||||
|
private DynamicECPreKeyMigrationConfiguration ecPreKeyMigrationConfiguration;
|
||||||
private KeysManager keysManager;
|
private KeysManager keysManager;
|
||||||
|
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
|
@ -41,12 +46,21 @@ class KeysManagerTest {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setup() {
|
void setup() {
|
||||||
|
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
||||||
|
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
|
||||||
|
ecPreKeyMigrationConfiguration = mock(DynamicECPreKeyMigrationConfiguration.class);
|
||||||
|
|
||||||
|
when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration);
|
||||||
|
when(dynamicConfiguration.getEcPreKeyMigrationConfiguration()).thenReturn(ecPreKeyMigrationConfiguration);
|
||||||
|
when(ecPreKeyMigrationConfiguration.storeEcSignedPreKeys()).thenReturn(true);
|
||||||
|
|
||||||
keysManager = new KeysManager(
|
keysManager = new KeysManager(
|
||||||
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
|
||||||
Tables.EC_KEYS.tableName(),
|
Tables.EC_KEYS.tableName(),
|
||||||
Tables.PQ_KEYS.tableName(),
|
Tables.PQ_KEYS.tableName(),
|
||||||
Tables.REPEATED_USE_EC_SIGNED_PRE_KEYS.tableName(),
|
Tables.REPEATED_USE_EC_SIGNED_PRE_KEYS.tableName(),
|
||||||
Tables.REPEATED_USE_KEM_SIGNED_PRE_KEYS.tableName());
|
Tables.REPEATED_USE_KEM_SIGNED_PRE_KEYS.tableName(),
|
||||||
|
dynamicConfigurationManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -251,6 +265,24 @@ class KeysManagerTest {
|
||||||
Set.copyOf(keysManager.getPqEnabledDevices(ACCOUNT_UUID)));
|
Set.copyOf(keysManager.getPqEnabledDevices(ACCOUNT_UUID)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testStoreEcSignedPreKeyDisabled() {
|
||||||
|
when(ecPreKeyMigrationConfiguration.storeEcSignedPreKeys()).thenReturn(false);
|
||||||
|
|
||||||
|
final ECKeyPair identityKeyPair = Curve.generateKeyPair();
|
||||||
|
|
||||||
|
keysManager.store(ACCOUNT_UUID, DEVICE_ID,
|
||||||
|
List.of(generateTestPreKey(1)),
|
||||||
|
List.of(KeysHelper.signedKEMPreKey(2, identityKeyPair)),
|
||||||
|
KeysHelper.signedECPreKey(3, identityKeyPair),
|
||||||
|
KeysHelper.signedKEMPreKey(4, identityKeyPair));
|
||||||
|
|
||||||
|
assertEquals(1, keysManager.getEcCount(ACCOUNT_UUID, DEVICE_ID));
|
||||||
|
assertEquals(1, keysManager.getPqCount(ACCOUNT_UUID, DEVICE_ID));
|
||||||
|
assertTrue(keysManager.getLastResort(ACCOUNT_UUID, DEVICE_ID).isPresent());
|
||||||
|
assertFalse(keysManager.getEcSignedPreKey(ACCOUNT_UUID, DEVICE_ID).join().isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
private static ECPreKey generateTestPreKey(final long keyId) {
|
private static ECPreKey generateTestPreKey(final long keyId) {
|
||||||
return new ECPreKey(keyId, Curve.generateKeyPair().getPublicKey());
|
return new ECPreKey(keyId, Curve.generateKeyPair().getPublicKey());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue