Convert RegistrationRecoveryPasswords to sync DynamoDB client

This commit is contained in:
Chris Eager 2026-05-19 17:47:05 -05:00 committed by Jon Chambers
parent 66b0ed16d1
commit fea4300d7d
22 changed files with 125 additions and 171 deletions

View File

@ -8,7 +8,10 @@ package org.signal.integration;
import java.time.Clock;
import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.signal.integration.config.Config;
import org.whispersystems.textsecuregcm.metrics.NoopAwsSdkMetricPublisher;
import org.whispersystems.textsecuregcm.registration.VerificationSession;
@ -18,7 +21,6 @@ import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
import org.whispersystems.textsecuregcm.storage.VerificationSessions;
import org.whispersystems.textsecuregcm.util.Util;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
@ -44,7 +46,7 @@ public class IntegrationTools {
config.dynamoDbClient().buildSyncClient(credentialsProvider, new NoopAwsSdkMetricPublisher());
final RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
config.dynamoDbTables().registrationRecovery(), Duration.ofDays(1), dynamoDbAsyncClient, Clock.systemUTC());
config.dynamoDbTables().registrationRecovery(), Duration.ofDays(1), dynamoDbClient, Clock.systemUTC());
final VerificationSessions verificationSessions = new VerificationSessions(
dynamoDbClient, config.dynamoDbTables().verificationSessions(), Clock.systemUTC());
@ -68,11 +70,14 @@ public class IntegrationTools {
this.changeNumberWaitingPeriods = changeNumberWaitingPeriods;
}
public CompletableFuture<Void> populateRecoveryPassword(final String phoneNumber, final byte[] password) {
return phoneNumberIdentifiers
.getPhoneNumberIdentifier(phoneNumber)
.thenCompose(pni -> registrationRecoveryPasswordsManager.store(pni, password))
.thenRun(Util.NOOP);
public void populateRecoveryPassword(final String phoneNumber, final byte[] password) {
try {
final UUID pni = phoneNumberIdentifiers
.getPhoneNumberIdentifier(phoneNumber).get(5, TimeUnit.SECONDS);
registrationRecoveryPasswordsManager.store(pni, password);
} catch (ExecutionException | InterruptedException | TimeoutException e) {
throw new RuntimeException("failed to get pni", e);
}
}
public Optional<String> peekVerificationSessionPushChallenge(final String sessionId) {

View File

@ -123,7 +123,7 @@ public final class Operations {
public static byte[] populateRandomRecoveryPassword(final String number) {
final byte[] recoveryPassword = randomBytes(32);
INTEGRATION_TOOLS.populateRecoveryPassword(number, recoveryPassword).join();
INTEGRATION_TOOLS.populateRecoveryPassword(number, recoveryPassword);
return recoveryPassword;
}

View File

@ -516,7 +516,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
config.getDynamoDbTables().getRegistrationRecovery().getTableName(),
config.getDynamoDbTables().getRegistrationRecovery().getExpiration(),
dynamoDbAsyncClient,
dynamoDbClient,
clock);
final VerificationSessions verificationSessions = new VerificationSessions(dynamoDbClient,

View File

@ -15,6 +15,7 @@ import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Response;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@ -129,10 +130,11 @@ public class PhoneVerificationTokenManager {
}
try {
final boolean verified = phoneNumberIdentifiers.getPhoneNumberIdentifier(number)
.thenCompose(phoneNumberIdentifier -> registrationRecoveryPasswordsManager.verify(phoneNumberIdentifier, recoveryPassword))
final UUID phoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(number)
.get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
final boolean verified = registrationRecoveryPasswordsManager.verify(phoneNumberIdentifier, recoveryPassword);
if (!verified) {
throw new ForbiddenException("recoveryPassword couldn't be verified");
}

View File

@ -152,7 +152,7 @@ public class RegistrationLockVerificationManager {
// This allows users to re-register via registration recovery password
// instead of always being forced to fall back to SMS verification.
if (!phoneVerificationType.equals(PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD) || clientRegistrationLock != null) {
registrationRecoveryPasswordsManager.remove(updatedAccount.getIdentifier(IdentityType.PNI)).join();
registrationRecoveryPasswordsManager.remove(updatedAccount.getIdentifier(IdentityType.PNI));
}
final List<Byte> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();

View File

@ -244,8 +244,7 @@ public class AccountController {
// if registration recovery password was sent to us, store it (or refresh its expiration)
attributes.recoveryPassword().ifPresent(registrationRecoveryPassword -> {
final boolean rrpCreated = registrationRecoveryPasswordsManager
.store(updatedAccount.getIdentifier(IdentityType.PNI), registrationRecoveryPassword)
.join();
.store(updatedAccount.getIdentifier(IdentityType.PNI), registrationRecoveryPassword);
Metrics.counter(RECOVERY_PASSWORD_SET_COUNTER_NAME, Tags.of(
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of("outcome", rrpCreated ? "created" : "updated")))

View File

@ -793,7 +793,7 @@ public class VerificationController {
// the RRP. It's possible the client will not actually be able to register (e.g. failed reglock challenge), and
// so we will have removed the RRP unnecessarily. The impact of this is low, since the owner of the RRP
// can always just fallback to session-based verification.
existingRRP = registrationRecoveryPasswordsManager.remove(phoneNumberIdentifiers.getPhoneNumberIdentifier(registrationServiceSession.number()).join()).join();
existingRRP = registrationRecoveryPasswordsManager.remove(phoneNumberIdentifiers.getPhoneNumberIdentifier(registrationServiceSession.number()).join());
}
Optional<Account> maybeExistingAccount;
@ -869,7 +869,7 @@ public class VerificationController {
// the RRP. It's possible the client will not actually be able to register (e.g. failed reglock challenge), and
// so we will have removed the RRP unnecessarily. The impact of this is low, since the owner of the RRP
// can always just fallback to session-based verification.
registrationRecoveryPasswordsManager.remove(phoneNumberIdentifiers.getPhoneNumberIdentifier(registrationServiceSession.number()).join()).join();
registrationRecoveryPasswordsManager.remove(phoneNumberIdentifiers.getPhoneNumberIdentifier(registrationServiceSession.number()).join());
}
return registrationServiceSession;

View File

@ -263,8 +263,7 @@ public class AccountsGrpcService extends SimpleAccountsGrpc.AccountsImplBase {
@Override
public SetRegistrationRecoveryPasswordResponse setRegistrationRecoveryPassword(final SetRegistrationRecoveryPasswordRequest request) {
registrationRecoveryPasswordsManager.store(getAuthenticatedAccount().getIdentifier(IdentityType.PNI),
request.getRegistrationRecoveryPassword().toByteArray())
.join();
request.getRegistrationRecoveryPassword().toByteArray());
return SetRegistrationRecoveryPasswordResponse.getDefaultInstance();
}

View File

@ -413,8 +413,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
final boolean rrpCreated = accountAttributes.recoveryPassword().map(registrationRecoveryPassword ->
registrationRecoveryPasswordsManager
.store(account.getIdentifier(IdentityType.PNI), registrationRecoveryPassword)
.join())
.store(account.getIdentifier(IdentityType.PNI), registrationRecoveryPassword))
.orElse(false);
changeNumberWaitingPeriodManager.handleAccountCreated(account.getUuid(), clock.instant());
@ -1214,10 +1213,11 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
keysManager.deleteSingleUsePreKeys(account.getUuid()),
keysManager.deleteSingleUsePreKeys(account.getPhoneNumberIdentifier()),
messagesManager.clear(account.getUuid()),
profilesManager.deleteAll(account.getUuid(), true),
registrationRecoveryPasswordsManager.remove(account.getIdentifier(IdentityType.PNI)))
profilesManager.deleteAll(account.getUuid(), true))
.join();
registrationRecoveryPasswordsManager.remove(account.getIdentifier(IdentityType.PNI));
accounts.delete(account.getUuid(), additionalWriteItems);
redisDelete(account);

View File

@ -13,15 +13,16 @@ import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.Util;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
import software.amazon.awssdk.services.dynamodb.model.DeleteItemResponse;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
import software.amazon.awssdk.services.dynamodb.model.PutItemResponse;
import software.amazon.awssdk.services.dynamodb.model.ReturnValue;
public class RegistrationRecoveryPasswords {
@ -36,31 +37,32 @@ public class RegistrationRecoveryPasswords {
private final Duration expiration;
private final DynamoDbAsyncClient asyncClient;
private final DynamoDbClient dynamoDbClient;
private final Clock clock;
public RegistrationRecoveryPasswords(
final String tableName,
final Duration expiration,
final DynamoDbAsyncClient asyncClient,
final DynamoDbClient dynamoDbClient,
final Clock clock) {
this.tableName = requireNonNull(tableName);
this.expiration = requireNonNull(expiration);
this.asyncClient = requireNonNull(asyncClient);
this.dynamoDbClient = requireNonNull(dynamoDbClient);
this.clock = requireNonNull(clock);
}
public CompletableFuture<Optional<SaltedTokenHash>> lookup(final UUID phoneNumberIdentifier) {
return asyncClient.getItem(GetItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_PNI, AttributeValues.fromString(phoneNumberIdentifier.toString())))
.consistentRead(true)
.build())
.thenApply(getItemResponse -> Optional.ofNullable(getItemResponse.item())
.filter(item -> item.containsKey(ATTR_SALT))
.filter(item -> item.containsKey(ATTR_HASH))
.map(RegistrationRecoveryPasswords::saltedTokenHashFromItem));
public Optional<SaltedTokenHash> lookup(final UUID phoneNumberIdentifier) {
final GetItemResponse getItemResponse = dynamoDbClient.getItem(GetItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_PNI, AttributeValues.fromString(phoneNumberIdentifier.toString())))
.consistentRead(true)
.build());
return Optional.ofNullable(getItemResponse.item())
.filter(item -> item.containsKey(ATTR_SALT))
.filter(item -> item.containsKey(ATTR_HASH))
.map(RegistrationRecoveryPasswords::saltedTokenHashFromItem);
}
/// Add a PNI -> RRP mapping, or replace the current one if it already exists
@ -68,31 +70,33 @@ public class RegistrationRecoveryPasswords {
/// @param phoneNumberIdentifier The PNI to associate the salted RRP with
/// @param data The salted registration recovery password
/// @return true if a new mapping was added, false if an existing mapping was updated
public CompletableFuture<Boolean> addOrReplace(final UUID phoneNumberIdentifier, final SaltedTokenHash data) {
public boolean addOrReplace(final UUID phoneNumberIdentifier, final SaltedTokenHash data) {
final long expirationSeconds = expirationSeconds();
return asyncClient.putItem(PutItemRequest.builder()
.tableName(tableName)
.returnValues(ReturnValue.ALL_OLD)
.item(Map.of(
KEY_PNI, AttributeValues.fromString(phoneNumberIdentifier.toString()),
ATTR_EXP, AttributeValues.fromLong(expirationSeconds),
ATTR_SALT, AttributeValues.fromString(data.salt()),
ATTR_HASH, AttributeValues.fromString(data.hash())))
.build())
.thenApply(response -> response.attributes() == null || response.attributes().isEmpty());
final PutItemResponse response = dynamoDbClient.putItem(PutItemRequest.builder()
.tableName(tableName)
.returnValues(ReturnValue.ALL_OLD)
.item(Map.of(
KEY_PNI, AttributeValues.fromString(phoneNumberIdentifier.toString()),
ATTR_EXP, AttributeValues.fromLong(expirationSeconds),
ATTR_SALT, AttributeValues.fromString(data.salt()),
ATTR_HASH, AttributeValues.fromString(data.hash())))
.build());
return response.attributes() == null || response.attributes().isEmpty();
}
/// Remove the entry associated with the provided PNI
///
/// @return true if an entry was removed, false if no entry existed
public CompletableFuture<Boolean> removeEntry(final UUID phoneNumberIdentifier) {
return asyncClient.deleteItem(DeleteItemRequest.builder()
.tableName(tableName)
.returnValues(ReturnValue.ALL_OLD)
.key(Map.of(KEY_PNI, AttributeValues.fromString(phoneNumberIdentifier.toString())))
.build())
.thenApply(response -> response.attributes() != null && !response.attributes().isEmpty());
public boolean removeEntry(final UUID phoneNumberIdentifier) {
final DeleteItemResponse response = dynamoDbClient.deleteItem(DeleteItemRequest.builder()
.tableName(tableName)
.returnValues(ReturnValue.ALL_OLD)
.key(Map.of(KEY_PNI, AttributeValues.fromString(phoneNumberIdentifier.toString())))
.build());
return response.attributes() != null && !response.attributes().isEmpty();
}
@VisibleForTesting

View File

@ -7,56 +7,32 @@ package org.whispersystems.textsecuregcm.storage;
import static java.util.Objects.requireNonNull;
import java.lang.invoke.MethodHandles;
import java.util.HexFormat;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException;
public class RegistrationRecoveryPasswordsManager {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final RegistrationRecoveryPasswords registrationRecoveryPasswords;
public RegistrationRecoveryPasswordsManager(final RegistrationRecoveryPasswords registrationRecoveryPasswords) {
this.registrationRecoveryPasswords = requireNonNull(registrationRecoveryPasswords);
}
public CompletableFuture<Boolean> verify(final UUID phoneNumberIdentifier, final byte[] password) {
public boolean verify(final UUID phoneNumberIdentifier, final byte[] password) {
return registrationRecoveryPasswords.lookup(phoneNumberIdentifier)
.thenApply(maybeHash -> maybeHash.filter(hash -> hash.verify(bytesToString(password))))
.whenComplete((_, error) -> {
if (error != null) {
logger.warn("Failed to lookup Registration Recovery Password", error);
}
})
.thenApply(Optional::isPresent);
.filter(hash -> hash.verify(bytesToString(password))).isPresent();
}
public CompletableFuture<Boolean> store(final UUID phoneNumberIdentifier, final byte[] password) {
public boolean store(final UUID phoneNumberIdentifier, final byte[] password) {
final String token = bytesToString(password);
final SaltedTokenHash tokenHash = SaltedTokenHash.generateFor(token);
return registrationRecoveryPasswords.addOrReplace(phoneNumberIdentifier, tokenHash)
.whenComplete((_, error) -> {
if (error != null) {
logger.warn("Failed to store Registration Recovery Password", error);
}
});
return registrationRecoveryPasswords.addOrReplace(phoneNumberIdentifier, tokenHash);
}
public CompletableFuture<Boolean> remove(final UUID phoneNumberIdentifier) {
return registrationRecoveryPasswords.removeEntry(phoneNumberIdentifier)
.whenComplete((_, error) -> {
if (error != null) {
logger.warn("Failed to remove Registration Recovery Password", error);
}
});
public boolean remove(final UUID phoneNumberIdentifier) {
return registrationRecoveryPasswords.removeEntry(phoneNumberIdentifier);
}
private static String bytesToString(final byte[] bytes) {

View File

@ -208,7 +208,7 @@ public record CommandDependencies(
RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
configuration.getDynamoDbTables().getRegistrationRecovery().getTableName(),
configuration.getDynamoDbTables().getRegistrationRecovery().getExpiration(),
dynamoDbAsyncClient,
dynamoDbClient,
clock);
Accounts accounts = new Accounts(

View File

@ -45,6 +45,7 @@ class PhoneVerificationTokenManagerTest {
private RegistrationServiceClient registrationServiceClient;
private RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
private RegistrationRecoveryChecker registrationRecoveryChecker;
private PhoneNumberIdentifiers phoneNumberIdentifiers;
private PhoneVerificationTokenManager phoneVerificationTokenManager;
@ -55,7 +56,7 @@ class PhoneVerificationTokenManagerTest {
@BeforeEach
void setUp() {
final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(PHONE_NUMBER))
.thenReturn(CompletableFuture.completedFuture(PHONE_NUMBER_IDENTIFIER));
@ -166,7 +167,7 @@ class PhoneVerificationTokenManagerTest {
.thenReturn(true);
when(registrationRecoveryPasswordsManager.verify(PHONE_NUMBER_IDENTIFIER, recoveryPassword))
.thenReturn(CompletableFuture.completedFuture(true));
.thenReturn(true);
assertDoesNotThrow(() -> phoneVerificationTokenManager.verify(containerRequestContext,
PHONE_NUMBER,
@ -183,7 +184,7 @@ class PhoneVerificationTokenManagerTest {
.thenReturn(false);
when(registrationRecoveryPasswordsManager.verify(PHONE_NUMBER_IDENTIFIER, recoveryPassword))
.thenReturn(CompletableFuture.completedFuture(true));
.thenReturn(true);
assertThrows(ForbiddenException.class, () -> phoneVerificationTokenManager.verify(containerRequestContext,
PHONE_NUMBER,
@ -200,7 +201,7 @@ class PhoneVerificationTokenManagerTest {
.thenReturn(true);
when(registrationRecoveryPasswordsManager.verify(PHONE_NUMBER_IDENTIFIER, recoveryPassword))
.thenReturn(CompletableFuture.completedFuture(false));
.thenReturn(false);
assertThrows(ForbiddenException.class, () -> phoneVerificationTokenManager.verify(containerRequestContext,
PHONE_NUMBER,
@ -210,7 +211,7 @@ class PhoneVerificationTokenManagerTest {
@ParameterizedTest
@MethodSource
void verifyRecoveryPasswordManagerException(final Throwable recoveryPasswordManagerException) {
void verifyPhoneNumberIdentifiersException(final Throwable phoneNumberIdentifiersException) {
final ContainerRequestContext containerRequestContext = mock(ContainerRequestContext.class);
final byte[] recoveryPassword = TestRandomUtil.nextBytes(16);
@ -218,8 +219,8 @@ class PhoneVerificationTokenManagerTest {
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(containerRequestContext, PHONE_NUMBER))
.thenReturn(true);
when(registrationRecoveryPasswordsManager.verify(PHONE_NUMBER_IDENTIFIER, recoveryPassword))
.thenReturn(CompletableFuture.failedFuture(recoveryPasswordManagerException));
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(PHONE_NUMBER))
.thenReturn(CompletableFuture.failedFuture(phoneNumberIdentifiersException));
assertThrows(ServerErrorException.class, () -> phoneVerificationTokenManager.verify(containerRequestContext,
PHONE_NUMBER,
@ -227,7 +228,7 @@ class PhoneVerificationTokenManagerTest {
recoveryPassword));
}
private static List<Throwable> verifyRecoveryPasswordManagerException() {
private static List<Throwable> verifyPhoneNumberIdentifiersException() {
return List.of(new ExecutionException(new RuntimeException()), new TimeoutException());
}
}

View File

@ -22,7 +22,6 @@ import static org.mockito.Mockito.when;
import jakarta.ws.rs.WebApplicationException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.annotation.Nullable;
@ -99,8 +98,6 @@ class RegistrationLockVerificationManagerTest {
when(account.hasLockedCredentials()).thenReturn(alreadyLocked);
doThrow(new NotPushRegisteredException()).when(pushNotificationManager).sendAttemptLoginNotification(any(), any());
when(registrationRecoveryPasswordsManager.remove(any())).thenReturn(CompletableFuture.completedFuture(true));
final Pair<Class<? extends Exception>, Consumer<Exception>> exceptionType = switch (error) {
case MISMATCH -> {
when(existingRegistrationLock.verify(clientRegistrationLock)).thenReturn(false);

View File

@ -833,8 +833,6 @@ class AccountControllerTest {
void testAccountsAttributesUpdateRecoveryPassword() {
final byte[] recoveryPassword = TestRandomUtil.nextBytes(32);
when(registrationRecoveryPasswordsManager.store(any(), any()))
.thenReturn(CompletableFuture.completedFuture(true));
try (final Response response = resources.getJerseyTest()
.target("/v1/accounts/attributes/")
.request()

View File

@ -618,9 +618,6 @@ class VerificationControllerTest {
.thenReturn(Optional.of(new VerificationSession(encodedSessionId, "challenge", null, List.of(), List.of(), null, null, true,
clock.millis(), clock.millis(), registrationServiceSession.expiration())));
when(registrationRecoveryPasswordsManager.remove(PNI))
.thenReturn(CompletableFuture.completedFuture(null));
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/verification/session/" + encodedSessionId)
.request()
@ -918,8 +915,6 @@ class VerificationControllerTest {
registrationServiceSession)));
when(verificationSessionManager.findForId(any()))
.thenReturn(Optional.of(mock(VerificationSession.class)));
when(registrationRecoveryPasswordsManager.remove(PNI))
.thenReturn(CompletableFuture.completedFuture(null));
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/verification/session/" + encodedSessionId)
@ -945,7 +940,6 @@ class VerificationControllerTest {
clock.millis(), clock.millis(), registrationServiceSession.expiration())));
when(registrationServiceClient.sendVerificationCode(any(), any(), any(), any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(registrationServiceSession));
when(registrationRecoveryPasswordsManager.remove(PNI)).thenReturn(CompletableFuture.completedFuture(null));
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/verification/session/" + encodedSessionId + "/code")
@ -1247,8 +1241,6 @@ class VerificationControllerTest {
when(verificationSessionManager.findForId(any()))
.thenReturn(Optional.of(new VerificationSession(encodedSessionId, null, null, Collections.emptyList(), Collections.emptyList(), null, null, true,
clock.millis(), clock.millis(), registrationServiceSession.expiration())));
when(registrationRecoveryPasswordsManager.remove(PNI))
.thenReturn(CompletableFuture.completedFuture(null));
final Invocation.Builder request = resources.getJerseyTest()
.target("/v1/verification/session/" + encodedSessionId + "/code")
@ -1375,8 +1367,6 @@ class VerificationControllerTest {
when(verificationSessionManager.findForId(any()))
.thenReturn(Optional.of(new VerificationSession(encodedSessionId, null, null, Collections.emptyList(), Collections.emptyList(), null, null, true,
clock.millis(), clock.millis(), registrationServiceSession.expiration())));
when(registrationRecoveryPasswordsManager.remove(any()))
.thenReturn(CompletableFuture.completedFuture(true));
final RegistrationServiceSession verifiedSession = new RegistrationServiceSession(SESSION_ID, NUMBER, true, null,
null, 0L,

View File

@ -26,7 +26,6 @@ import java.util.HexFormat;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@ -106,9 +105,6 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
when(rateLimiters.getSetZkCredentialKeyLimiter()).thenReturn(rateLimiter);
when(registrationRecoveryPasswordsManager.store(any(), any()))
.thenReturn(CompletableFuture.completedFuture(null));
return new AccountsGrpcService(accountsManager,
rateLimiters,
usernameHashZkProofVerifier,

View File

@ -134,9 +134,6 @@ public class AccountCreationDeletionIntegrationTest {
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
mock(RegistrationRecoveryPasswordsManager.class);
when(registrationRecoveryPasswordsManager.remove(any()))
.thenReturn(CompletableFuture.completedFuture(null));
disconnectionRequestManager = mock(DisconnectionRequestManager.class);
when(disconnectionRequestManager.requestDisconnection(any())).thenReturn(CompletableFuture.completedFuture(null));
@ -513,7 +510,7 @@ public class AccountCreationDeletionIntegrationTest {
() -> assertNull(primaryDevice.getApnId()));
maybeGcmRegistrationId.ifPresentOrElse(
gcmRegistrationId -> assertEquals(deliveryChannels.fcmToken(), primaryDevice.getGcmId()),
_ -> assertEquals(deliveryChannels.fcmToken(), primaryDevice.getGcmId()),
() -> assertNull(primaryDevice.getGcmId()));
assertTrue(account.getRegistrationLock().verify(registrationLockSecret));

View File

@ -126,9 +126,6 @@ class AccountsManagerChangeNumberIntegrationTest {
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
mock(RegistrationRecoveryPasswordsManager.class);
when(registrationRecoveryPasswordsManager.remove(any()))
.thenReturn(CompletableFuture.completedFuture(null));
accountsManager = new AccountsManager(
accounts,
phoneNumberIdentifiers,

View File

@ -197,8 +197,6 @@ class AccountsManagerTest {
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
mock(RegistrationRecoveryPasswordsManager.class);
when(registrationRecoveryPasswordsManager.remove(any())).thenReturn(CompletableFuture.completedFuture(null));
when(keysManager.deleteSingleUsePreKeys(any())).thenReturn(CompletableFuture.completedFuture(null));
when(messagesManager.clear(any())).thenReturn(CompletableFuture.completedFuture(null));
when(profilesManager.deleteAll(any(), anyBoolean())).thenReturn(CompletableFuture.completedFuture(null));

View File

@ -131,9 +131,6 @@ public class AddRemoveDeviceIntegrationTest {
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
mock(RegistrationRecoveryPasswordsManager.class);
when(registrationRecoveryPasswordsManager.remove(any()))
.thenReturn(CompletableFuture.completedFuture(null));
PUBSUB_SERVER_EXTENSION.getRedisClient().useConnection(connection -> {
connection.sync().flushall();
connection.sync().configSet("notify-keyspace-events", "K$");

View File

@ -15,7 +15,6 @@ import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -27,6 +26,7 @@ import org.whispersystems.textsecuregcm.util.MockUtils;
import org.whispersystems.textsecuregcm.util.MutableClock;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
public class RegistrationRecoveryTest {
@ -51,7 +51,7 @@ public class RegistrationRecoveryTest {
registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
Tables.REGISTRATION_RECOVERY_PASSWORDS.tableName(),
EXPIRATION,
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
DYNAMO_DB_EXTENSION.getDynamoDbClient(),
CLOCK
);
@ -60,12 +60,12 @@ public class RegistrationRecoveryTest {
@Test
public void testLookupAfterWrite() throws Exception {
assertTrue(registrationRecoveryPasswords.addOrReplace(PNI, ORIGINAL_HASH).get());
assertTrue(registrationRecoveryPasswords.addOrReplace(PNI, ORIGINAL_HASH));
final long initialExp = fetchTimestamp(PNI);
final long expectedExpiration = CLOCK.instant().getEpochSecond() + EXPIRATION.getSeconds();
assertEquals(expectedExpiration, initialExp);
final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(PNI).get();
final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(PNI);
assertTrue(saltedTokenHashByPni.isPresent());
assertEquals(ORIGINAL_HASH.salt(), saltedTokenHashByPni.get().salt());
assertEquals(ORIGINAL_HASH.hash(), saltedTokenHashByPni.get().hash());
@ -73,83 +73,81 @@ public class RegistrationRecoveryTest {
@Test
public void testLookupAfterRefresh() throws Exception {
registrationRecoveryPasswords.addOrReplace(PNI, ORIGINAL_HASH).get();
registrationRecoveryPasswords.addOrReplace(PNI, ORIGINAL_HASH);
CLOCK.increment(50, TimeUnit.SECONDS);
registrationRecoveryPasswords.addOrReplace(PNI, ORIGINAL_HASH).get();
registrationRecoveryPasswords.addOrReplace(PNI, ORIGINAL_HASH);
final long updatedExp = fetchTimestamp(PNI);
final long expectedExp = CLOCK.instant().getEpochSecond() + EXPIRATION.getSeconds();
assertEquals(expectedExp, updatedExp);
final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(PNI).get();
final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(PNI);
assertTrue(saltedTokenHashByPni.isPresent());
assertEquals(ORIGINAL_HASH.salt(), saltedTokenHashByPni.get().salt());
assertEquals(ORIGINAL_HASH.hash(), saltedTokenHashByPni.get().hash());
}
@Test
public void testReplace() throws Exception {
assertTrue(registrationRecoveryPasswords.addOrReplace(PNI, ORIGINAL_HASH).get());
assertFalse(registrationRecoveryPasswords.addOrReplace(PNI, ANOTHER_HASH).get());
public void testReplace() {
assertTrue(registrationRecoveryPasswords.addOrReplace(PNI, ORIGINAL_HASH));
assertFalse(registrationRecoveryPasswords.addOrReplace(PNI, ANOTHER_HASH));
final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(PNI).get();
final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(PNI);
assertTrue(saltedTokenHashByPni.isPresent());
assertEquals(ANOTHER_HASH.salt(), saltedTokenHashByPni.get().salt());
assertEquals(ANOTHER_HASH.hash(), saltedTokenHashByPni.get().hash());
}
@Test
public void testRemove() throws Exception {
assertFalse(registrationRecoveryPasswords.removeEntry(PNI).join());
public void testRemove() {
assertFalse(registrationRecoveryPasswords.removeEntry(PNI));
registrationRecoveryPasswords.addOrReplace(PNI, ORIGINAL_HASH).get();
assertTrue(registrationRecoveryPasswords.lookup(PNI).get().isPresent());
registrationRecoveryPasswords.addOrReplace(PNI, ORIGINAL_HASH);
assertTrue(registrationRecoveryPasswords.lookup(PNI).isPresent());
assertTrue(registrationRecoveryPasswords.removeEntry(PNI).get());
assertTrue(registrationRecoveryPasswords.lookup(PNI).get().isEmpty());
assertTrue(registrationRecoveryPasswords.removeEntry(PNI));
assertTrue(registrationRecoveryPasswords.lookup(PNI).isEmpty());
}
@Test
public void testManagerFlow() throws Exception {
public void testManagerFlow() {
final byte[] password = "password".getBytes(StandardCharsets.UTF_8);
final byte[] updatedPassword = "udpate".getBytes(StandardCharsets.UTF_8);
final byte[] wrongPassword = "qwerty123".getBytes(StandardCharsets.UTF_8);
// initial store
manager.store(PNI, password).get();
assertTrue(manager.verify(PNI, password).get());
assertFalse(manager.verify(PNI, wrongPassword).get());
manager.store(PNI, password);
assertTrue(manager.verify(PNI, password));
assertFalse(manager.verify(PNI, wrongPassword));
// update
manager.store(PNI, password).get();
assertTrue(manager.verify(PNI, password).get());
assertFalse(manager.verify(PNI, wrongPassword).get());
manager.store(PNI, password);
assertTrue(manager.verify(PNI, password));
assertFalse(manager.verify(PNI, wrongPassword));
// replace
manager.store(PNI, updatedPassword).get();
assertTrue(manager.verify(PNI, updatedPassword).get());
assertFalse(manager.verify(PNI, password).get());
assertFalse(manager.verify(PNI, wrongPassword).get());
manager.store(PNI, updatedPassword);
assertTrue(manager.verify(PNI, updatedPassword));
assertFalse(manager.verify(PNI, password));
assertFalse(manager.verify(PNI, wrongPassword));
manager.remove(PNI).get();
assertFalse(manager.verify(PNI, updatedPassword).get());
assertFalse(manager.verify(PNI, password).get());
assertFalse(manager.verify(PNI, wrongPassword).get());
manager.remove(PNI);
assertFalse(manager.verify(PNI, updatedPassword));
assertFalse(manager.verify(PNI, password));
assertFalse(manager.verify(PNI, wrongPassword));
}
private static long fetchTimestamp(final UUID phoneNumberIdentifier) throws ExecutionException, InterruptedException {
return DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient().getItem(GetItemRequest.builder()
private static long fetchTimestamp(final UUID phoneNumberIdentifier) {
final GetItemResponse getItemResponse = DYNAMO_DB_EXTENSION.getDynamoDbClient().getItem(GetItemRequest.builder()
.tableName(Tables.REGISTRATION_RECOVERY_PASSWORDS.tableName())
.key(Map.of(RegistrationRecoveryPasswords.KEY_PNI, AttributeValues.fromString(phoneNumberIdentifier.toString())))
.build())
.thenApply(getItemResponse -> {
final Map<String, AttributeValue> item = getItemResponse.item();
if (item == null || !item.containsKey(RegistrationRecoveryPasswords.ATTR_EXP)) {
throw new RuntimeException("Data not found");
}
final String exp = item.get(RegistrationRecoveryPasswords.ATTR_EXP).n();
return Long.parseLong(exp);
})
.get();
.build());
final Map<String, AttributeValue> item = getItemResponse.item();
if (item == null || !item.containsKey(RegistrationRecoveryPasswords.ATTR_EXP)) {
throw new RuntimeException("Data not found");
}
final String exp = item.get(RegistrationRecoveryPasswords.ATTR_EXP).n();
return Long.parseLong(exp);
}
}