Read registration recovery passwords exclusively by PNI

This commit is contained in:
Jon Chambers 2024-11-25 17:05:20 -05:00 committed by Jon Chambers
parent 6967e4e54b
commit 5b9f8177f2
19 changed files with 129 additions and 118 deletions

View File

@ -8,6 +8,7 @@ package org.signal.integration;
import java.time.Clock; import java.time.Clock;
import java.time.Duration; import java.time.Duration;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import org.signal.integration.config.Config; import org.signal.integration.config.Config;
import org.whispersystems.textsecuregcm.metrics.NoopAwsSdkMetricPublisher; import org.whispersystems.textsecuregcm.metrics.NoopAwsSdkMetricPublisher;
@ -20,7 +21,6 @@ import org.whispersystems.textsecuregcm.storage.VerificationSessions;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
public class IntegrationTools { public class IntegrationTools {
@ -57,8 +57,8 @@ public class IntegrationTools {
this.verificationSessionManager = verificationSessionManager; this.verificationSessionManager = verificationSessionManager;
} }
public CompletableFuture<Void> populateRecoveryPassword(final String e164, final byte[] password) { public CompletableFuture<Void> populateRecoveryPassword(final String number, final byte[] password) {
return registrationRecoveryPasswordsManager.storeForCurrentNumber(e164, password); return registrationRecoveryPasswordsManager.storeForCurrentNumber(number, password);
} }
public CompletableFuture<Optional<String>> peekVerificationSessionPushChallenge(final String sessionId) { public CompletableFuture<Optional<String>> peekVerificationSessionPushChallenge(final String sessionId) {

View File

@ -78,7 +78,7 @@ public final class Operations {
final TestUser user = TestUser.create(number, accountPassword, registrationPassword); final TestUser user = TestUser.create(number, accountPassword, registrationPassword);
final AccountAttributes accountAttributes = user.accountAttributes(); final AccountAttributes accountAttributes = user.accountAttributes();
INTEGRATION_TOOLS.populateRecoveryPassword(number, registrationPassword).join(); INTEGRATION_TOOLS.populateRecoveryPassword(user.phoneNumber(), registrationPassword).join();
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair(); final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair(); final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();

View File

@ -1076,7 +1076,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
final PhoneVerificationTokenManager phoneVerificationTokenManager = new PhoneVerificationTokenManager( final PhoneVerificationTokenManager phoneVerificationTokenManager = new PhoneVerificationTokenManager(
registrationServiceClient, registrationRecoveryPasswordsManager, registrationRecoveryChecker); phoneNumberIdentifiers, registrationServiceClient, registrationRecoveryPasswordsManager, registrationRecoveryChecker);
final List<Object> commonControllers = Lists.newArrayList( final List<Object> commonControllers = Lists.newArrayList(
new AccountController(accountsManager, rateLimiters, turnTokenGenerator, registrationRecoveryPasswordsManager, new AccountController(accountsManager, rateLimiters, turnTokenGenerator, registrationRecoveryPasswordsManager,
usernameHashZkProofVerifier), usernameHashZkProofVerifier),
@ -1119,8 +1119,9 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
config.getCdnConfiguration().credentials().secretAccessKey().value(), config.getCdnConfiguration().region(), config.getCdnConfiguration().credentials().secretAccessKey().value(), config.getCdnConfiguration().region(),
config.getCdnConfiguration().bucket()), config.getCdnConfiguration().bucket()),
new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions), new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions),
pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters, pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager,
accountsManager, registrationFraudChecker, dynamicConfigurationManager, clock) phoneNumberIdentifiers, rateLimiters, accountsManager, registrationFraudChecker,
dynamicConfigurationManager, clock)
); );
if (config.getSubscription() != null && config.getOneTimeDonations() != null) { if (config.getSubscription() != null && config.getOneTimeDonations() != null) {
SubscriptionManager subscriptionManager = new SubscriptionManager(subscriptions, SubscriptionManager subscriptionManager = new SubscriptionManager(subscriptions,

View File

@ -25,6 +25,7 @@ import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession; import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient; import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
import org.whispersystems.textsecuregcm.spam.RegistrationRecoveryChecker; import org.whispersystems.textsecuregcm.spam.RegistrationRecoveryChecker;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
public class PhoneVerificationTokenManager { public class PhoneVerificationTokenManager {
@ -33,13 +34,17 @@ public class PhoneVerificationTokenManager {
private static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15); private static final Duration REGISTRATION_RPC_TIMEOUT = Duration.ofSeconds(15);
private static final long VERIFICATION_TIMEOUT_SECONDS = REGISTRATION_RPC_TIMEOUT.plusSeconds(1).getSeconds(); private static final long VERIFICATION_TIMEOUT_SECONDS = REGISTRATION_RPC_TIMEOUT.plusSeconds(1).getSeconds();
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
private final RegistrationServiceClient registrationServiceClient; private final RegistrationServiceClient registrationServiceClient;
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager; private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
private final RegistrationRecoveryChecker registrationRecoveryChecker; private final RegistrationRecoveryChecker registrationRecoveryChecker;
public PhoneVerificationTokenManager(final RegistrationServiceClient registrationServiceClient, public PhoneVerificationTokenManager(final PhoneNumberIdentifiers phoneNumberIdentifiers,
final RegistrationServiceClient registrationServiceClient,
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager, final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
final RegistrationRecoveryChecker registrationRecoveryChecker) { final RegistrationRecoveryChecker registrationRecoveryChecker) {
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
this.registrationServiceClient = registrationServiceClient; this.registrationServiceClient = registrationServiceClient;
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager; this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
this.registrationRecoveryChecker = registrationRecoveryChecker; this.registrationRecoveryChecker = registrationRecoveryChecker;
@ -109,7 +114,7 @@ public class PhoneVerificationTokenManager {
throw new ForbiddenException("recoveryPassword couldn't be verified"); throw new ForbiddenException("recoveryPassword couldn't be verified");
} }
try { try {
final boolean verified = registrationRecoveryPasswordsManager.verify(number, recoveryPassword) final boolean verified = registrationRecoveryPasswordsManager.verify(phoneNumberIdentifiers.getPhoneNumberIdentifier(number).join(), recoveryPassword)
.get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS); .get(VERIFICATION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
if (!verified) { if (!verified) {
throw new ForbiddenException("recoveryPassword couldn't be verified"); throw new ForbiddenException("recoveryPassword couldn't be verified");

View File

@ -23,6 +23,7 @@ import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest; import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure; import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
import org.whispersystems.textsecuregcm.entities.Svr3Credentials; import org.whispersystems.textsecuregcm.entities.Svr3Credentials;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;

View File

@ -83,6 +83,7 @@ import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker;
import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker.VerificationCheck; import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker.VerificationCheck;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager; import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
import org.whispersystems.textsecuregcm.util.ExceptionUtils; import org.whispersystems.textsecuregcm.util.ExceptionUtils;
@ -116,6 +117,7 @@ public class VerificationController {
private final PushNotificationManager pushNotificationManager; private final PushNotificationManager pushNotificationManager;
private final RegistrationCaptchaManager registrationCaptchaManager; private final RegistrationCaptchaManager registrationCaptchaManager;
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager; private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
private final RateLimiters rateLimiters; private final RateLimiters rateLimiters;
private final AccountsManager accountsManager; private final AccountsManager accountsManager;
private final RegistrationFraudChecker registrationFraudChecker; private final RegistrationFraudChecker registrationFraudChecker;
@ -127,6 +129,7 @@ public class VerificationController {
final PushNotificationManager pushNotificationManager, final PushNotificationManager pushNotificationManager,
final RegistrationCaptchaManager registrationCaptchaManager, final RegistrationCaptchaManager registrationCaptchaManager,
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager, final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
final PhoneNumberIdentifiers phoneNumberIdentifiers,
final RateLimiters rateLimiters, final RateLimiters rateLimiters,
final AccountsManager accountsManager, final AccountsManager accountsManager,
final RegistrationFraudChecker registrationFraudChecker, final RegistrationFraudChecker registrationFraudChecker,
@ -137,6 +140,7 @@ public class VerificationController {
this.pushNotificationManager = pushNotificationManager; this.pushNotificationManager = pushNotificationManager;
this.registrationCaptchaManager = registrationCaptchaManager; this.registrationCaptchaManager = registrationCaptchaManager;
this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager; this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
this.rateLimiters = rateLimiters; this.rateLimiters = rateLimiters;
this.accountsManager = accountsManager; this.accountsManager = accountsManager;
this.registrationFraudChecker = registrationFraudChecker; this.registrationFraudChecker = registrationFraudChecker;
@ -626,8 +630,8 @@ public class VerificationController {
} }
/** /**
* @throws ClientErrorException with {@code 422} status if the ID cannot be decoded * @throws ClientErrorException with {@code 422} status if the ID cannot be decoded
* @throws javax.ws.rs.NotFoundException if the ID cannot be found * @throws NotFoundException if the ID cannot be found
*/ */
private RegistrationServiceSession retrieveRegistrationServiceSession(final String encodedSessionId) { private RegistrationServiceSession retrieveRegistrationServiceSession(final String encodedSessionId) {
final byte[] sessionId; final byte[] sessionId;

View File

@ -9,8 +9,11 @@ import java.util.Base64;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.signal.registration.rpc.RegistrationSessionMetadata; import org.signal.registration.rpc.RegistrationSessionMetadata;
public record RegistrationServiceSession(byte[] id, String number, boolean verified, public record RegistrationServiceSession(byte[] id,
@Nullable Long nextSms, @Nullable Long nextVoiceCall, String number,
boolean verified,
@Nullable Long nextSms,
@Nullable Long nextVoiceCall,
@Nullable Long nextVerificationAttempt, @Nullable Long nextVerificationAttempt,
long expiration) { long expiration) {

View File

@ -47,6 +47,7 @@ import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
import org.whispersystems.textsecuregcm.controllers.AccountController; import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.entities.EncryptedUsername; import org.whispersystems.textsecuregcm.entities.EncryptedUsername;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;

View File

@ -16,7 +16,6 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.util.AttributeValues; import org.whispersystems.textsecuregcm.util.AttributeValues;
import org.whispersystems.textsecuregcm.util.Pair;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue; import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
@ -28,9 +27,8 @@ import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
public class RegistrationRecoveryPasswords { public class RegistrationRecoveryPasswords {
// As a temporary transitional measure, this can be either a string representation of an E164-formatted phone number // For historical reasons, we record the PNI as a UUID string rather than a compact byte array
// or a UUID (PNI) string static final String KEY_PNI = "P";
static final String KEY_E164 = "P";
static final String ATTR_EXP = "E"; static final String ATTR_EXP = "E";
static final String ATTR_SALT = "S"; static final String ATTR_SALT = "S";
static final String ATTR_HASH = "H"; static final String ATTR_HASH = "H";
@ -54,10 +52,10 @@ public class RegistrationRecoveryPasswords {
this.clock = requireNonNull(clock); this.clock = requireNonNull(clock);
} }
public CompletableFuture<Optional<SaltedTokenHash>> lookup(final String number) { public CompletableFuture<Optional<SaltedTokenHash>> lookup(final UUID phoneNumberIdentifier) {
return asyncClient.getItem(GetItemRequest.builder() return asyncClient.getItem(GetItemRequest.builder()
.tableName(tableName) .tableName(tableName)
.key(Map.of(KEY_E164, AttributeValues.fromString(number))) .key(Map.of(KEY_PNI, AttributeValues.fromString(phoneNumberIdentifier.toString())))
.consistentRead(true) .consistentRead(true)
.build()) .build())
.thenApply(getItemResponse -> Optional.ofNullable(getItemResponse.item()) .thenApply(getItemResponse -> Optional.ofNullable(getItemResponse.item())
@ -66,10 +64,6 @@ public class RegistrationRecoveryPasswords {
.map(RegistrationRecoveryPasswords::saltedTokenHashFromItem)); .map(RegistrationRecoveryPasswords::saltedTokenHashFromItem));
} }
public CompletableFuture<Optional<SaltedTokenHash>> lookup(final UUID phoneNumberIdentifier) {
return lookup(phoneNumberIdentifier.toString());
}
public CompletableFuture<Void> addOrReplace(final String number, final UUID phoneNumberIdentifier, final SaltedTokenHash data) { public CompletableFuture<Void> addOrReplace(final String number, final UUID phoneNumberIdentifier, final SaltedTokenHash data) {
final long expirationSeconds = expirationSeconds(); final long expirationSeconds = expirationSeconds();
@ -90,7 +84,7 @@ public class RegistrationRecoveryPasswords {
.put(Put.builder() .put(Put.builder()
.tableName(tableName) .tableName(tableName)
.item(Map.of( .item(Map.of(
KEY_E164, AttributeValues.fromString(key), KEY_PNI, AttributeValues.fromString(key),
ATTR_EXP, AttributeValues.fromLong(expirationSeconds), ATTR_EXP, AttributeValues.fromLong(expirationSeconds),
ATTR_SALT, AttributeValues.fromString(salt), ATTR_SALT, AttributeValues.fromString(salt),
ATTR_HASH, AttributeValues.fromString(hash))) ATTR_HASH, AttributeValues.fromString(hash)))
@ -111,7 +105,7 @@ public class RegistrationRecoveryPasswords {
return TransactWriteItem.builder() return TransactWriteItem.builder()
.delete(Delete.builder() .delete(Delete.builder()
.tableName(tableName) .tableName(tableName)
.key(Map.of(KEY_E164, AttributeValues.fromString(key))) .key(Map.of(KEY_PNI, AttributeValues.fromString(key)))
.build()) .build())
.build(); .build();
} }

View File

@ -10,6 +10,7 @@ import static java.util.Objects.requireNonNull;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -30,8 +31,8 @@ public class RegistrationRecoveryPasswordsManager {
this.phoneNumberIdentifiers = phoneNumberIdentifiers; this.phoneNumberIdentifiers = phoneNumberIdentifiers;
} }
public CompletableFuture<Boolean> verify(final String number, final byte[] password) { public CompletableFuture<Boolean> verify(final UUID phoneNumberIdentifier, final byte[] password) {
return registrationRecoveryPasswords.lookup(number) return registrationRecoveryPasswords.lookup(phoneNumberIdentifier)
.thenApply(maybeHash -> maybeHash.filter(hash -> hash.verify(bytesToString(password)))) .thenApply(maybeHash -> maybeHash.filter(hash -> hash.verify(bytesToString(password))))
.whenComplete((result, error) -> { .whenComplete((result, error) -> {
if (error != null) { if (error != null) {

View File

@ -31,11 +31,11 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest; import org.whispersystems.textsecuregcm.entities.PhoneVerificationRequest;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException; import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushNotificationManager; import org.whispersystems.textsecuregcm.push.PushNotificationManager;
import org.whispersystems.textsecuregcm.push.WebSocketConnectionEventManager;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
@ -53,7 +53,7 @@ class RegistrationLockVerificationManagerTest {
ExternalServiceCredentialsGenerator.class); ExternalServiceCredentialsGenerator.class);
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock( private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock(
RegistrationRecoveryPasswordsManager.class); RegistrationRecoveryPasswordsManager.class);
private static PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class); private final PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class);
private final RateLimiters rateLimiters = mock(RateLimiters.class); private final RateLimiters rateLimiters = mock(RateLimiters.class);
private final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager( private final RegistrationLockVerificationManager registrationLockVerificationManager = new RegistrationLockVerificationManager(
accountsManager, disconnectionRequestManager, svr2CredentialsGenerator, accountsManager, disconnectionRequestManager, svr2CredentialsGenerator,
@ -105,12 +105,13 @@ class RegistrationLockVerificationManagerTest {
if (!verificationType.equals(PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD) || clientRegistrationLock != null) { if (!verificationType.equals(PhoneVerificationRequest.VerificationType.RECOVERY_PASSWORD) || clientRegistrationLock != null) {
verify(registrationRecoveryPasswordsManager).removeForNumber(account.getNumber()); verify(registrationRecoveryPasswordsManager).removeForNumber(account.getNumber());
} else { } else {
verify(registrationRecoveryPasswordsManager, never()).removeForNumber(account.getNumber()); verify(registrationRecoveryPasswordsManager, never()).removeForNumber(any());
} }
verify(disconnectionRequestManager).requestDisconnection(account.getUuid(), List.of(Device.PRIMARY_ID)); verify(disconnectionRequestManager).requestDisconnection(account.getUuid(), List.of(Device.PRIMARY_ID));
try { try {
verify(pushNotificationManager).sendAttemptLoginNotification(any(), eq("failedRegistrationLock")); verify(pushNotificationManager).sendAttemptLoginNotification(any(), eq("failedRegistrationLock"));
} catch (NotPushRegisteredException npre) {} } catch (final NotPushRegisteredException ignored) {
}
if (alreadyLocked) { if (alreadyLocked) {
verify(account, never()).lockAuthTokenHash(); verify(account, never()).lockAuthTokenHash();
} else { } else {
@ -126,10 +127,13 @@ class RegistrationLockVerificationManagerTest {
doThrow(RateLimitExceededException.class).when(pinLimiter).validate(anyString()); doThrow(RateLimitExceededException.class).when(pinLimiter).validate(anyString());
yield new Pair<>(RateLimitExceededException.class, ignored -> { yield new Pair<>(RateLimitExceededException.class, ignored -> {
verify(account, never()).lockAuthTokenHash(); verify(account, never()).lockAuthTokenHash();
try { try {
verify(pushNotificationManager, never()).sendAttemptLoginNotification(any(), eq("failedRegistrationLock")); verify(pushNotificationManager, never()).sendAttemptLoginNotification(any(), eq("failedRegistrationLock"));
} catch (NotPushRegisteredException npre) {} } catch (final NotPushRegisteredException ignored2) {
verify(registrationRecoveryPasswordsManager, never()).removeForNumber(account.getNumber()); }
verify(registrationRecoveryPasswordsManager, never()).removeForNumber(any());
verify(disconnectionRequestManager, never()).requestDisconnection(any(), any()); verify(disconnectionRequestManager, never()).requestDisconnection(any(), any());
}); });
} }
@ -167,7 +171,7 @@ class RegistrationLockVerificationManagerTest {
PhoneVerificationRequest.VerificationType.SESSION)); PhoneVerificationRequest.VerificationType.SESSION));
verify(account, never()).lockAuthTokenHash(); verify(account, never()).lockAuthTokenHash();
verify(registrationRecoveryPasswordsManager, never()).removeForNumber(account.getNumber()); verify(registrationRecoveryPasswordsManager, never()).removeForNumber(any());
verify(disconnectionRequestManager, never()).requestDisconnection(any(), any()); verify(disconnectionRequestManager, never()).requestDisconnection(any(), any());
} }

View File

@ -86,6 +86,7 @@ import org.whispersystems.textsecuregcm.storage.AccountBadge;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.ChangeNumberManager; import org.whispersystems.textsecuregcm.storage.ChangeNumberManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
@ -105,6 +106,7 @@ class AccountControllerV2Test {
private final AccountsManager accountsManager = mock(AccountsManager.class); private final AccountsManager accountsManager = mock(AccountsManager.class);
private final ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class); private final ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class);
private final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
private final RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class); private final RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class);
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock( private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock(
RegistrationRecoveryPasswordsManager.class); RegistrationRecoveryPasswordsManager.class);
@ -125,8 +127,8 @@ class AccountControllerV2Test {
.setTestContainerFactory(new GrizzlyWebTestContainerFactory()) .setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource( .addResource(
new AccountControllerV2(accountsManager, changeNumberManager, new AccountControllerV2(accountsManager, changeNumberManager,
new PhoneVerificationTokenManager(registrationServiceClient, registrationRecoveryPasswordsManager, new PhoneVerificationTokenManager(phoneNumberIdentifiers, registrationServiceClient,
registrationRecoveryChecker), registrationRecoveryPasswordsManager, registrationRecoveryChecker),
registrationLockVerificationManager, rateLimiters)) registrationLockVerificationManager, rateLimiters))
.build(); .build();
@ -401,6 +403,8 @@ class AccountControllerV2Test {
@Test @Test
void recoveryPasswordManagerVerificationTrue() throws Exception { void recoveryPasswordManagerVerificationTrue() throws Exception {
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(any()))
.thenReturn(CompletableFuture.completedFuture(UUID.randomUUID()));
when(registrationRecoveryPasswordsManager.verify(any(), any())) when(registrationRecoveryPasswordsManager.verify(any(), any()))
.thenReturn(CompletableFuture.completedFuture(true)); .thenReturn(CompletableFuture.completedFuture(true));
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())) when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any()))
@ -443,6 +447,8 @@ class AccountControllerV2Test {
@Test @Test
void registrationRecoveryCheckerAllowsAttempt() { void registrationRecoveryCheckerAllowsAttempt() {
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(any()))
.thenReturn(CompletableFuture.completedFuture(UUID.randomUUID()));
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true); when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true);
when(registrationRecoveryPasswordsManager.verify(any(), any())) when(registrationRecoveryPasswordsManager.verify(any(), any()))
.thenReturn(CompletableFuture.completedFuture(true)); .thenReturn(CompletableFuture.completedFuture(true));

View File

@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.doThrow;
@ -77,6 +78,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.DeviceCapability; import org.whispersystems.textsecuregcm.storage.DeviceCapability;
import org.whispersystems.textsecuregcm.storage.DeviceSpec; import org.whispersystems.textsecuregcm.storage.DeviceSpec;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.tests.util.KeysHelper; import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
@ -94,6 +96,7 @@ class RegistrationControllerTest {
private static final String PASSWORD = "password"; private static final String PASSWORD = "password";
private final AccountsManager accountsManager = mock(AccountsManager.class); private final AccountsManager accountsManager = mock(AccountsManager.class);
private final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
private final RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class); private final RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class);
private final RegistrationLockVerificationManager registrationLockVerificationManager = mock( private final RegistrationLockVerificationManager registrationLockVerificationManager = mock(
RegistrationLockVerificationManager.class); RegistrationLockVerificationManager.class);
@ -113,8 +116,8 @@ class RegistrationControllerTest {
.setTestContainerFactory(new GrizzlyWebTestContainerFactory()) .setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource( .addResource(
new RegistrationController(accountsManager, new RegistrationController(accountsManager,
new PhoneVerificationTokenManager(registrationServiceClient, registrationRecoveryPasswordsManager, new PhoneVerificationTokenManager(phoneNumberIdentifiers, registrationServiceClient,
registrationRecoveryChecker), registrationRecoveryPasswordsManager, registrationRecoveryChecker),
registrationLockVerificationManager, rateLimiters)) registrationLockVerificationManager, rateLimiters))
.build(); .build();
@ -243,6 +246,8 @@ class RegistrationControllerTest {
@Test @Test
void recoveryPasswordManagerVerificationFailureOrTimeout() { void recoveryPasswordManagerVerificationFailureOrTimeout() {
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(any()))
.thenReturn(CompletableFuture.completedFuture(UUID.randomUUID()));
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true); when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true);
when(registrationRecoveryPasswordsManager.verify(any(), any())) when(registrationRecoveryPasswordsManager.verify(any(), any()))
.thenReturn(CompletableFuture.failedFuture(new RuntimeException())); .thenReturn(CompletableFuture.failedFuture(new RuntimeException()));
@ -289,6 +294,8 @@ class RegistrationControllerTest {
@Test @Test
void recoveryPasswordManagerVerificationTrue() throws InterruptedException { void recoveryPasswordManagerVerificationTrue() throws InterruptedException {
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(any()))
.thenReturn(CompletableFuture.completedFuture(UUID.randomUUID()));
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true); when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true);
when(registrationRecoveryPasswordsManager.verify(any(), any())) when(registrationRecoveryPasswordsManager.verify(any(), any()))
.thenReturn(CompletableFuture.completedFuture(true)); .thenReturn(CompletableFuture.completedFuture(true));
@ -325,6 +332,8 @@ class RegistrationControllerTest {
@Test @Test
void registrationRecoveryCheckerAllowsAttempt() throws InterruptedException { void registrationRecoveryCheckerAllowsAttempt() throws InterruptedException {
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(any()))
.thenReturn(CompletableFuture.completedFuture(UUID.randomUUID()));
when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true); when(registrationRecoveryChecker.checkRegistrationRecoveryAttempt(any(), any())).thenReturn(true);
when(registrationRecoveryPasswordsManager.verify(any(), any())) when(registrationRecoveryPasswordsManager.verify(any(), any()))
.thenReturn(CompletableFuture.completedFuture(true)); .thenReturn(CompletableFuture.completedFuture(true));

View File

@ -39,6 +39,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -77,6 +78,7 @@ import org.whispersystems.textsecuregcm.spam.RegistrationFraudChecker;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager; import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
import org.whispersystems.textsecuregcm.storage.VerificationSessionManager; import org.whispersystems.textsecuregcm.storage.VerificationSessionManager;
import org.whispersystems.textsecuregcm.util.SystemMapper; import org.whispersystems.textsecuregcm.util.SystemMapper;
@ -87,7 +89,11 @@ class VerificationControllerTest {
private static final long SESSION_EXPIRATION_SECONDS = Duration.ofMinutes(10).toSeconds(); private static final long SESSION_EXPIRATION_SECONDS = Duration.ofMinutes(10).toSeconds();
private static final byte[] SESSION_ID = "session".getBytes(StandardCharsets.UTF_8); private static final byte[] SESSION_ID = "session".getBytes(StandardCharsets.UTF_8);
private static final String NUMBER = "+18005551212"; private static final String NUMBER = PhoneNumberUtil.getInstance().format(
PhoneNumberUtil.getInstance().getExampleNumber("US"),
PhoneNumberUtil.PhoneNumberFormat.E164);
private static final UUID PNI = UUID.randomUUID();
private final RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class); private final RegistrationServiceClient registrationServiceClient = mock(RegistrationServiceClient.class);
private final VerificationSessionManager verificationSessionManager = mock(VerificationSessionManager.class); private final VerificationSessionManager verificationSessionManager = mock(VerificationSessionManager.class);
@ -95,6 +101,7 @@ class VerificationControllerTest {
private final RegistrationCaptchaManager registrationCaptchaManager = mock(RegistrationCaptchaManager.class); private final RegistrationCaptchaManager registrationCaptchaManager = mock(RegistrationCaptchaManager.class);
private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock( private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock(
RegistrationRecoveryPasswordsManager.class); RegistrationRecoveryPasswordsManager.class);
private final PhoneNumberIdentifiers phoneNumberIdentifiers = mock(PhoneNumberIdentifiers.class);
private final RateLimiters rateLimiters = mock(RateLimiters.class); private final RateLimiters rateLimiters = mock(RateLimiters.class);
private final AccountsManager accountsManager = mock(AccountsManager.class); private final AccountsManager accountsManager = mock(AccountsManager.class);
private final Clock clock = Clock.systemUTC(); private final Clock clock = Clock.systemUTC();
@ -115,7 +122,7 @@ class VerificationControllerTest {
.setTestContainerFactory(new GrizzlyWebTestContainerFactory()) .setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource( .addResource(
new VerificationController(registrationServiceClient, verificationSessionManager, pushNotificationManager, new VerificationController(registrationServiceClient, verificationSessionManager, pushNotificationManager,
registrationCaptchaManager, registrationRecoveryPasswordsManager, rateLimiters, accountsManager, registrationCaptchaManager, registrationRecoveryPasswordsManager, phoneNumberIdentifiers, rateLimiters, accountsManager,
RegistrationFraudChecker.noop(), dynamicConfigurationManager, clock)) RegistrationFraudChecker.noop(), dynamicConfigurationManager, clock))
.build(); .build();
@ -131,6 +138,8 @@ class VerificationControllerTest {
.thenReturn(new DynamicRegistrationConfiguration(false)); .thenReturn(new DynamicRegistrationConfiguration(false));
when(dynamicConfigurationManager.getConfiguration()) when(dynamicConfigurationManager.getConfiguration())
.thenReturn(dynamicConfiguration); .thenReturn(dynamicConfiguration);
when(phoneNumberIdentifiers.getPhoneNumberIdentifier(NUMBER))
.thenReturn(CompletableFuture.completedFuture(PNI));
} }
@ParameterizedTest @ParameterizedTest

View File

@ -65,6 +65,7 @@ import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.EncryptedUsername; import org.whispersystems.textsecuregcm.entities.EncryptedUsername;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
@ -113,7 +114,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
when(rateLimiter.validateReactive(any(UUID.class))).thenReturn(Mono.empty()); when(rateLimiter.validateReactive(any(UUID.class))).thenReturn(Mono.empty());
when(rateLimiter.validateReactive(anyString())).thenReturn(Mono.empty()); when(rateLimiter.validateReactive(anyString())).thenReturn(Mono.empty());
when(registrationRecoveryPasswordsManager.storeForCurrentNumber(anyString(), any())) when(registrationRecoveryPasswordsManager.storeForCurrentNumber(any(), any()))
.thenReturn(CompletableFuture.completedFuture(null)); .thenReturn(CompletableFuture.completedFuture(null));
return new AccountsGrpcService(accountsManager, return new AccountsGrpcService(accountsManager,
@ -261,7 +262,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
final List<byte[]> usernameHashes = invocation.getArgument(1); final List<byte[]> usernameHashes = invocation.getArgument(1);
return CompletableFuture.completedFuture( return CompletableFuture.completedFuture(
new AccountsManager.UsernameReservation(invocation.getArgument(0), usernameHashes.get(0))); new AccountsManager.UsernameReservation(invocation.getArgument(0), usernameHashes.getFirst()));
}); });
final ReserveUsernameHashResponse expectedResponse = ReserveUsernameHashResponse.newBuilder() final ReserveUsernameHashResponse expectedResponse = ReserveUsernameHashResponse.newBuilder()
@ -684,12 +685,10 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
@Test @Test
void setRegistrationRecoveryPassword() { void setRegistrationRecoveryPassword() {
final String phoneNumber = final UUID phoneNumberIdentifier = UUID.randomUUID();
PhoneNumberUtil.getInstance().format(PhoneNumberUtil.getInstance().getExampleNumber("US"),
PhoneNumberUtil.PhoneNumberFormat.E164);
final Account account = mock(Account.class); final Account account = mock(Account.class);
when(account.getNumber()).thenReturn(phoneNumber); when(account.getIdentifier(IdentityType.PNI)).thenReturn(phoneNumberIdentifier);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI)) when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account))); .thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
@ -701,7 +700,7 @@ class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, Ac
.setRegistrationRecoveryPassword(ByteString.copyFrom(registrationRecoveryPassword)) .setRegistrationRecoveryPassword(ByteString.copyFrom(registrationRecoveryPassword))
.build())); .build()));
verify(registrationRecoveryPasswordsManager).storeForCurrentNumber(phoneNumber, registrationRecoveryPassword); verify(registrationRecoveryPasswordsManager).storeForCurrentNumber(account.getNumber(), registrationRecoveryPassword);
} }
@Test @Test

View File

@ -224,7 +224,7 @@ class AccountsManagerTest {
final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
mock(RegistrationRecoveryPasswordsManager.class); mock(RegistrationRecoveryPasswordsManager.class);
when(registrationRecoveryPasswordsManager.removeForNumber(anyString())).thenReturn(CompletableFuture.completedFuture(null)); when(registrationRecoveryPasswordsManager.removeForNumber(any())).thenReturn(CompletableFuture.completedFuture(null));
when(keysManager.deleteSingleUsePreKeys(any())).thenReturn(CompletableFuture.completedFuture(null)); when(keysManager.deleteSingleUsePreKeys(any())).thenReturn(CompletableFuture.completedFuture(null));
when(messagesManager.clear(any())).thenReturn(CompletableFuture.completedFuture(null)); when(messagesManager.clear(any())).thenReturn(CompletableFuture.completedFuture(null));

View File

@ -288,10 +288,10 @@ public final class DynamoDbExtensionSchema {
List.of(), List.of()), List.of(), List.of()),
REGISTRATION_RECOVERY_PASSWORDS("registration_recovery_passwords_test", REGISTRATION_RECOVERY_PASSWORDS("registration_recovery_passwords_test",
RegistrationRecoveryPasswords.KEY_E164, RegistrationRecoveryPasswords.KEY_PNI,
null, null,
List.of(AttributeDefinition.builder() List.of(AttributeDefinition.builder()
.attributeName(RegistrationRecoveryPasswords.KEY_E164) .attributeName(RegistrationRecoveryPasswords.KEY_PNI)
.attributeType(ScalarAttributeType.S) .attributeType(ScalarAttributeType.S)
.build()), .build()),
List.of(), List.of()), List.of(), List.of()),

View File

@ -32,20 +32,17 @@ import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
public class RegistrationRecoveryTest { public class RegistrationRecoveryTest {
private static final MutableClock CLOCK = MockUtils.mutableClock(0); private static final MutableClock CLOCK = MockUtils.mutableClock(0);
private static final Duration EXPIRATION = Duration.ofSeconds(1000); private static final Duration EXPIRATION = Duration.ofSeconds(1000);
private static final String NUMBER = "+18005555555"; private static final String NUMBER = "+18005555555";
private static final UUID PNI = UUID.randomUUID();
private static final SaltedTokenHash ORIGINAL_HASH = SaltedTokenHash.generateFor("pass1"); private static final SaltedTokenHash ORIGINAL_HASH = SaltedTokenHash.generateFor("pass1");
private static final SaltedTokenHash ANOTHER_HASH = SaltedTokenHash.generateFor("pass2"); private static final SaltedTokenHash ANOTHER_HASH = SaltedTokenHash.generateFor("pass2");
@RegisterExtension @RegisterExtension
private static final DynamoDbExtension DYNAMO_DB_EXTENSION = new DynamoDbExtension( private static final DynamoDbExtension DYNAMO_DB_EXTENSION =
Tables.PNI, new DynamoDbExtension(Tables.PNI, Tables.REGISTRATION_RECOVERY_PASSWORDS);
Tables.REGISTRATION_RECOVERY_PASSWORDS);
private UUID pni;
private RegistrationRecoveryPasswords registrationRecoveryPasswords; private RegistrationRecoveryPasswords registrationRecoveryPasswords;
@ -60,89 +57,63 @@ public class RegistrationRecoveryTest {
DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(),
CLOCK CLOCK
); );
final PhoneNumberIdentifiers phoneNumberIdentifiers = final PhoneNumberIdentifiers phoneNumberIdentifiers =
new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), Tables.PNI.tableName()); new PhoneNumberIdentifiers(DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient(), Tables.PNI.tableName());
manager = new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords, phoneNumberIdentifiers); manager = new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords, phoneNumberIdentifiers);
pni = phoneNumberIdentifiers.getPhoneNumberIdentifier(NUMBER).join();
} }
@Test @Test
public void testLookupAfterWrite() throws Exception { public void testLookupAfterWrite() throws Exception {
registrationRecoveryPasswords.addOrReplace(NUMBER, PNI, ORIGINAL_HASH).get(); registrationRecoveryPasswords.addOrReplace(NUMBER, pni, ORIGINAL_HASH).get();
final long initialExp = fetchTimestamp(NUMBER); final long initialExp = fetchTimestamp(NUMBER);
final long expectedExpiration = CLOCK.instant().getEpochSecond() + EXPIRATION.getSeconds(); final long expectedExpiration = CLOCK.instant().getEpochSecond() + EXPIRATION.getSeconds();
assertEquals(expectedExpiration, initialExp); assertEquals(expectedExpiration, initialExp);
{ final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(pni).get();
final Optional<SaltedTokenHash> saltedTokenHashByNumber = registrationRecoveryPasswords.lookup(NUMBER).get(); assertTrue(saltedTokenHashByPni.isPresent());
assertTrue(saltedTokenHashByNumber.isPresent()); assertEquals(ORIGINAL_HASH.salt(), saltedTokenHashByPni.get().salt());
assertEquals(ORIGINAL_HASH.salt(), saltedTokenHashByNumber.get().salt()); assertEquals(ORIGINAL_HASH.hash(), saltedTokenHashByPni.get().hash());
assertEquals(ORIGINAL_HASH.hash(), saltedTokenHashByNumber.get().hash());
}
{
final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(PNI).get();
assertTrue(saltedTokenHashByPni.isPresent());
assertEquals(ORIGINAL_HASH.salt(), saltedTokenHashByPni.get().salt());
assertEquals(ORIGINAL_HASH.hash(), saltedTokenHashByPni.get().hash());
}
} }
@Test @Test
public void testLookupAfterRefresh() throws Exception { public void testLookupAfterRefresh() throws Exception {
registrationRecoveryPasswords.addOrReplace(NUMBER, PNI, ORIGINAL_HASH).get(); registrationRecoveryPasswords.addOrReplace(NUMBER, pni, ORIGINAL_HASH).get();
CLOCK.increment(50, TimeUnit.SECONDS); CLOCK.increment(50, TimeUnit.SECONDS);
registrationRecoveryPasswords.addOrReplace(NUMBER, PNI, ORIGINAL_HASH).get(); registrationRecoveryPasswords.addOrReplace(NUMBER, pni, ORIGINAL_HASH).get();
final long updatedExp = fetchTimestamp(NUMBER); final long updatedExp = fetchTimestamp(NUMBER);
final long expectedExp = CLOCK.instant().getEpochSecond() + EXPIRATION.getSeconds(); final long expectedExp = CLOCK.instant().getEpochSecond() + EXPIRATION.getSeconds();
assertEquals(expectedExp, updatedExp); assertEquals(expectedExp, updatedExp);
{ final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(pni).get();
final Optional<SaltedTokenHash> saltedTokenHashByNumber = registrationRecoveryPasswords.lookup(NUMBER).get(); assertTrue(saltedTokenHashByPni.isPresent());
assertTrue(saltedTokenHashByNumber.isPresent()); assertEquals(ORIGINAL_HASH.salt(), saltedTokenHashByPni.get().salt());
assertEquals(ORIGINAL_HASH.salt(), saltedTokenHashByNumber.get().salt()); assertEquals(ORIGINAL_HASH.hash(), saltedTokenHashByPni.get().hash());
assertEquals(ORIGINAL_HASH.hash(), saltedTokenHashByNumber.get().hash());
}
{
final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(PNI).get();
assertTrue(saltedTokenHashByPni.isPresent());
assertEquals(ORIGINAL_HASH.salt(), saltedTokenHashByPni.get().salt());
assertEquals(ORIGINAL_HASH.hash(), saltedTokenHashByPni.get().hash());
}
} }
@Test @Test
public void testReplace() throws Exception { public void testReplace() throws Exception {
registrationRecoveryPasswords.addOrReplace(NUMBER, PNI, ORIGINAL_HASH).get(); registrationRecoveryPasswords.addOrReplace(NUMBER, pni, ORIGINAL_HASH).get();
registrationRecoveryPasswords.addOrReplace(NUMBER, PNI, ANOTHER_HASH).get(); registrationRecoveryPasswords.addOrReplace(NUMBER, pni, ANOTHER_HASH).get();
{ final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(pni).get();
final Optional<SaltedTokenHash> saltedTokenHashByNumber = registrationRecoveryPasswords.lookup(NUMBER).get(); assertTrue(saltedTokenHashByPni.isPresent());
assertTrue(saltedTokenHashByNumber.isPresent()); assertEquals(ANOTHER_HASH.salt(), saltedTokenHashByPni.get().salt());
assertEquals(ANOTHER_HASH.salt(), saltedTokenHashByNumber.get().salt()); assertEquals(ANOTHER_HASH.hash(), saltedTokenHashByPni.get().hash());
assertEquals(ANOTHER_HASH.hash(), saltedTokenHashByNumber.get().hash());
}
{
final Optional<SaltedTokenHash> saltedTokenHashByPni = registrationRecoveryPasswords.lookup(PNI).get();
assertTrue(saltedTokenHashByPni.isPresent());
assertEquals(ANOTHER_HASH.salt(), saltedTokenHashByPni.get().salt());
assertEquals(ANOTHER_HASH.hash(), saltedTokenHashByPni.get().hash());
}
} }
@Test @Test
public void testRemove() throws Exception { public void testRemove() throws Exception {
assertDoesNotThrow(() -> registrationRecoveryPasswords.removeEntry(NUMBER, PNI).join()); assertDoesNotThrow(() -> registrationRecoveryPasswords.removeEntry(NUMBER, pni).join());
registrationRecoveryPasswords.addOrReplace(NUMBER, PNI, ORIGINAL_HASH).get(); registrationRecoveryPasswords.addOrReplace(NUMBER, pni, ORIGINAL_HASH).get();
assertTrue(registrationRecoveryPasswords.lookup(NUMBER).get().isPresent()); assertTrue(registrationRecoveryPasswords.lookup(pni).get().isPresent());
registrationRecoveryPasswords.removeEntry(NUMBER, PNI).get(); registrationRecoveryPasswords.removeEntry(NUMBER, pni).get();
assertTrue(registrationRecoveryPasswords.lookup(NUMBER).get().isEmpty()); assertTrue(registrationRecoveryPasswords.lookup(pni).get().isEmpty());
} }
@Test @Test
@ -153,30 +124,30 @@ public class RegistrationRecoveryTest {
// initial store // initial store
manager.storeForCurrentNumber(NUMBER, password).get(); manager.storeForCurrentNumber(NUMBER, password).get();
assertTrue(manager.verify(NUMBER, password).get()); assertTrue(manager.verify(pni, password).get());
assertFalse(manager.verify(NUMBER, wrongPassword).get()); assertFalse(manager.verify(pni, wrongPassword).get());
// update // update
manager.storeForCurrentNumber(NUMBER, password).get(); manager.storeForCurrentNumber(NUMBER, password).get();
assertTrue(manager.verify(NUMBER, password).get()); assertTrue(manager.verify(pni, password).get());
assertFalse(manager.verify(NUMBER, wrongPassword).get()); assertFalse(manager.verify(pni, wrongPassword).get());
// replace // replace
manager.storeForCurrentNumber(NUMBER, updatedPassword).get(); manager.storeForCurrentNumber(NUMBER, updatedPassword).get();
assertTrue(manager.verify(NUMBER, updatedPassword).get()); assertTrue(manager.verify(pni, updatedPassword).get());
assertFalse(manager.verify(NUMBER, password).get()); assertFalse(manager.verify(pni, password).get());
assertFalse(manager.verify(NUMBER, wrongPassword).get()); assertFalse(manager.verify(pni, wrongPassword).get());
manager.removeForNumber(NUMBER).get(); manager.removeForNumber(NUMBER).get();
assertFalse(manager.verify(NUMBER, updatedPassword).get()); assertFalse(manager.verify(pni, updatedPassword).get());
assertFalse(manager.verify(NUMBER, password).get()); assertFalse(manager.verify(pni, password).get());
assertFalse(manager.verify(NUMBER, wrongPassword).get()); assertFalse(manager.verify(pni, wrongPassword).get());
} }
private static long fetchTimestamp(final String number) throws ExecutionException, InterruptedException { private static long fetchTimestamp(final String number) throws ExecutionException, InterruptedException {
return DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient().getItem(GetItemRequest.builder() return DYNAMO_DB_EXTENSION.getDynamoDbAsyncClient().getItem(GetItemRequest.builder()
.tableName(Tables.REGISTRATION_RECOVERY_PASSWORDS.tableName()) .tableName(Tables.REGISTRATION_RECOVERY_PASSWORDS.tableName())
.key(Map.of(RegistrationRecoveryPasswords.KEY_E164, AttributeValues.fromString(number))) .key(Map.of(RegistrationRecoveryPasswords.KEY_PNI, AttributeValues.fromString(number)))
.build()) .build())
.thenApply(getItemResponse -> { .thenApply(getItemResponse -> {
final Map<String, AttributeValue> item = getItemResponse.item(); final Map<String, AttributeValue> item = getItemResponse.item();

View File

@ -85,6 +85,7 @@ public class AuthHelper {
public static final String UNDISCOVERABLE_NUMBER = "+18005551234"; public static final String UNDISCOVERABLE_NUMBER = "+18005551234";
public static final UUID UNDISCOVERABLE_UUID = UUID.randomUUID(); public static final UUID UNDISCOVERABLE_UUID = UUID.randomUUID();
public static final UUID UNDISCOVERABLE_PNI = UUID.randomUUID();
public static final String UNDISCOVERABLE_PASSWORD = "IT'S A SECRET TO EVERYBODY."; public static final String UNDISCOVERABLE_PASSWORD = "IT'S A SECRET TO EVERYBODY.";
public static final ECKeyPair VALID_IDENTITY_KEY_PAIR = Curve.generateKeyPair(); public static final ECKeyPair VALID_IDENTITY_KEY_PAIR = Curve.generateKeyPair();
@ -169,7 +170,9 @@ public class AuthHelper {
when(VALID_ACCOUNT_TWO.getPhoneNumberIdentifier()).thenReturn(VALID_PNI_TWO); when(VALID_ACCOUNT_TWO.getPhoneNumberIdentifier()).thenReturn(VALID_PNI_TWO);
when(UNDISCOVERABLE_ACCOUNT.getNumber()).thenReturn(UNDISCOVERABLE_NUMBER); when(UNDISCOVERABLE_ACCOUNT.getNumber()).thenReturn(UNDISCOVERABLE_NUMBER);
when(UNDISCOVERABLE_ACCOUNT.getUuid()).thenReturn(UNDISCOVERABLE_UUID); when(UNDISCOVERABLE_ACCOUNT.getUuid()).thenReturn(UNDISCOVERABLE_UUID);
when(UNDISCOVERABLE_ACCOUNT.getPhoneNumberIdentifier()).thenReturn(UNDISCOVERABLE_PNI);
when(UNDISCOVERABLE_ACCOUNT.getIdentifier(IdentityType.ACI)).thenReturn(UNDISCOVERABLE_UUID); when(UNDISCOVERABLE_ACCOUNT.getIdentifier(IdentityType.ACI)).thenReturn(UNDISCOVERABLE_UUID);
when(UNDISCOVERABLE_ACCOUNT.getIdentifier(IdentityType.PNI)).thenReturn(UNDISCOVERABLE_PNI);
when(VALID_ACCOUNT_3.getNumber()).thenReturn(VALID_NUMBER_3); when(VALID_ACCOUNT_3.getNumber()).thenReturn(VALID_NUMBER_3);
when(VALID_ACCOUNT_3.getUuid()).thenReturn(VALID_UUID_3); when(VALID_ACCOUNT_3.getUuid()).thenReturn(VALID_UUID_3);
when(VALID_ACCOUNT_3.getPhoneNumberIdentifier()).thenReturn(VALID_PNI_3); when(VALID_ACCOUNT_3.getPhoneNumberIdentifier()).thenReturn(VALID_PNI_3);