Convert RegistrationRecoveryPasswords to sync DynamoDB client
This commit is contained in:
parent
66b0ed16d1
commit
fea4300d7d
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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")))
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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$");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user