Write registration recovery passwords exclusively by PNI

This commit is contained in:
Jon Chambers
2024-11-26 17:06:48 -05:00
committed by Jon Chambers
parent 8be43566a4
commit 2803c2acdb
20 changed files with 92 additions and 133 deletions

View File

@@ -586,7 +586,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(
dynamicConfigurationManager);
RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords, phoneNumberIdentifiers);
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
UsernameHashZkProofVerifier usernameHashZkProofVerifier = new UsernameHashZkProofVerifier();
RegistrationServiceClient registrationServiceClient = config.getRegistrationServiceConfiguration()

View File

@@ -157,7 +157,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.removeForNumber(updatedAccount.getNumber());
registrationRecoveryPasswordsManager.remove(updatedAccount.getIdentifier(IdentityType.PNI));
}
final List<Byte> deviceIds = updatedAccount.getDevices().stream().map(Device::getId).toList();

View File

@@ -54,6 +54,7 @@ import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
import org.whispersystems.textsecuregcm.entities.UsernameLinkHandle;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.identity.IdentityType;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
@@ -259,7 +260,7 @@ public class AccountController {
// if registration recovery password was sent to us, store it (or refresh its expiration)
attributes.recoveryPassword().ifPresent(registrationRecoveryPassword ->
registrationRecoveryPasswordsManager.storeForCurrentNumber(updatedAccount.getNumber(), registrationRecoveryPassword));
registrationRecoveryPasswordsManager.store(updatedAccount.getIdentifier(IdentityType.PNI), registrationRecoveryPassword));
}
@GET

View File

@@ -602,7 +602,7 @@ public class VerificationController {
}
if (resultSession.verified()) {
registrationRecoveryPasswordsManager.removeForNumber(registrationServiceSession.number());
registrationRecoveryPasswordsManager.remove(phoneNumberIdentifiers.getPhoneNumberIdentifier(registrationServiceSession.number()).join());
}
Metrics.counter(VERIFIED_COUNTER_NAME, Tags.of(
@@ -648,7 +648,7 @@ public class VerificationController {
.orElseThrow(NotFoundException::new);
if (registrationServiceSession.verified()) {
registrationRecoveryPasswordsManager.removeForNumber(registrationServiceSession.number());
registrationRecoveryPasswordsManager.remove(phoneNumberIdentifiers.getPhoneNumberIdentifier(registrationServiceSession.number()).join());
}
return registrationServiceSession;

View File

@@ -337,7 +337,7 @@ public class AccountsGrpcService extends ReactorAccountsGrpc.AccountsImplBase {
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier()))
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
.flatMap(account -> Mono.fromFuture(() -> registrationRecoveryPasswordsManager.storeForCurrentNumber(account.getNumber(), request.getRegistrationRecoveryPassword().toByteArray())))
.flatMap(account -> Mono.fromFuture(() -> registrationRecoveryPasswordsManager.store(account.getIdentifier(IdentityType.PNI), request.getRegistrationRecoveryPassword().toByteArray())))
.thenReturn(SetRegistrationRecoveryPasswordResponse.newBuilder().build());
}
}

View File

@@ -385,7 +385,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
Metrics.counter(CREATE_COUNTER_NAME, tags).increment();
accountAttributes.recoveryPassword().ifPresent(registrationRecoveryPassword ->
registrationRecoveryPasswordsManager.storeForCurrentNumber(account.getNumber(),
registrationRecoveryPasswordsManager.store(account.getIdentifier(IdentityType.PNI),
registrationRecoveryPassword));
}, accountLockExecutor);
@@ -1279,7 +1279,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
keysManager.deleteSingleUsePreKeys(account.getPhoneNumberIdentifier()),
messagesManager.clear(account.getUuid()),
profilesManager.deleteAll(account.getUuid()),
registrationRecoveryPasswordsManager.removeForNumber(account.getNumber()))
registrationRecoveryPasswordsManager.remove(account.getIdentifier(IdentityType.PNI)))
.thenCompose(ignored -> accounts.delete(account.getUuid(), additionalWriteItems))
.thenCompose(ignored -> redisDeleteAsync(account))
.thenRun(() -> disconnectionRequestManager.requestDisconnection(account.getUuid()));

View File

@@ -19,11 +19,9 @@ 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.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.Delete;
import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest;
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
import software.amazon.awssdk.services.dynamodb.model.Put;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
public class RegistrationRecoveryPasswords {
@@ -64,50 +62,26 @@ public class RegistrationRecoveryPasswords {
.map(RegistrationRecoveryPasswords::saltedTokenHashFromItem));
}
public CompletableFuture<Void> addOrReplace(final String number, final UUID phoneNumberIdentifier, final SaltedTokenHash data) {
public CompletableFuture<Void> addOrReplace(final UUID phoneNumberIdentifier, final SaltedTokenHash data) {
final long expirationSeconds = expirationSeconds();
return asyncClient.transactWriteItems(TransactWriteItemsRequest.builder()
.transactItems(
buildPutRecoveryPasswordWriteItem(number, expirationSeconds, data.salt(), data.hash()),
buildPutRecoveryPasswordWriteItem(phoneNumberIdentifier.toString(), expirationSeconds, data.salt(), data.hash()))
.build())
.thenRun(Util.NOOP);
}
private TransactWriteItem buildPutRecoveryPasswordWriteItem(final String key,
final long expirationSeconds,
final String salt,
final String hash) {
return TransactWriteItem.builder()
.put(Put.builder()
return asyncClient.putItem(PutItemRequest.builder()
.tableName(tableName)
.item(Map.of(
KEY_PNI, AttributeValues.fromString(key),
KEY_PNI, AttributeValues.fromString(phoneNumberIdentifier.toString()),
ATTR_EXP, AttributeValues.fromLong(expirationSeconds),
ATTR_SALT, AttributeValues.fromString(salt),
ATTR_HASH, AttributeValues.fromString(hash)))
ATTR_SALT, AttributeValues.fromString(data.salt()),
ATTR_HASH, AttributeValues.fromString(data.hash())))
.build())
.build();
}
public CompletableFuture<Void> removeEntry(final String number, final UUID phoneNumberIdentifier) {
return asyncClient.transactWriteItems(TransactWriteItemsRequest.builder()
.transactItems(
buildDeleteRecoveryPasswordWriteItem(number),
buildDeleteRecoveryPasswordWriteItem(phoneNumberIdentifier.toString()))
.build())
.thenRun(Util.NOOP);
}
private TransactWriteItem buildDeleteRecoveryPasswordWriteItem(final String key) {
return TransactWriteItem.builder()
.delete(Delete.builder()
public CompletableFuture<Void> removeEntry(final UUID phoneNumberIdentifier) {
return asyncClient.deleteItem(DeleteItemRequest.builder()
.tableName(tableName)
.key(Map.of(KEY_PNI, AttributeValues.fromString(key)))
.key(Map.of(KEY_PNI, AttributeValues.fromString(phoneNumberIdentifier.toString())))
.build())
.build();
.thenRun(Util.NOOP);
}
@VisibleForTesting

View File

@@ -22,13 +22,9 @@ public class RegistrationRecoveryPasswordsManager {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private final RegistrationRecoveryPasswords registrationRecoveryPasswords;
private final PhoneNumberIdentifiers phoneNumberIdentifiers;
public RegistrationRecoveryPasswordsManager(final RegistrationRecoveryPasswords registrationRecoveryPasswords,
final PhoneNumberIdentifiers phoneNumberIdentifiers) {
public RegistrationRecoveryPasswordsManager(final RegistrationRecoveryPasswords registrationRecoveryPasswords) {
this.registrationRecoveryPasswords = requireNonNull(registrationRecoveryPasswords);
this.phoneNumberIdentifiers = phoneNumberIdentifiers;
}
public CompletableFuture<Boolean> verify(final UUID phoneNumberIdentifier, final byte[] password) {
@@ -42,30 +38,28 @@ public class RegistrationRecoveryPasswordsManager {
.thenApply(Optional::isPresent);
}
public CompletableFuture<Void> storeForCurrentNumber(final String number, final byte[] password) {
public CompletableFuture<Void> store(final UUID phoneNumberIdentifier, final byte[] password) {
final String token = bytesToString(password);
final SaltedTokenHash tokenHash = SaltedTokenHash.generateFor(token);
return phoneNumberIdentifiers.getPhoneNumberIdentifier(number)
.thenCompose(phoneNumberIdentifier -> registrationRecoveryPasswords.addOrReplace(number, phoneNumberIdentifier, tokenHash)
.whenComplete((result, error) -> {
if (error != null) {
logger.warn("Failed to store Registration Recovery Password", error);
}
}));
return registrationRecoveryPasswords.addOrReplace(phoneNumberIdentifier, tokenHash)
.whenComplete((result, error) -> {
if (error != null) {
logger.warn("Failed to store Registration Recovery Password", error);
}
});
}
public CompletableFuture<Void> removeForNumber(final String number) {
return phoneNumberIdentifiers.getPhoneNumberIdentifier(number)
.thenCompose(phoneNumberIdentifier -> registrationRecoveryPasswords.removeEntry(number, phoneNumberIdentifier)
.whenComplete((ignored, error) -> {
if (error instanceof ResourceNotFoundException) {
// These will naturally happen if a recovery password is already deleted. Since we can remove
// the recovery password through many flows, we avoid creating log messages for these exceptions
} else if (error != null) {
logger.warn("Failed to remove Registration Recovery Password", error);
}
}));
public CompletableFuture<Void> remove(final UUID phoneNumberIdentifier) {
return registrationRecoveryPasswords.removeEntry(phoneNumberIdentifier)
.whenComplete((ignored, error) -> {
if (error instanceof ResourceNotFoundException) {
// These will naturally happen if a recovery password is already deleted. Since we can remove
// the recovery password through many flows, we avoid creating log messages for these exceptions
} else if (error != null) {
logger.warn("Failed to remove Registration Recovery Password", error);
}
});
}
private static String bytesToString(final byte[] bytes) {

View File

@@ -223,7 +223,7 @@ record CommandDependencies(
ClientPublicKeysManager clientPublicKeysManager =
new ClientPublicKeysManager(clientPublicKeys, accountLockManager, accountLockExecutor);
RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager =
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords, phoneNumberIdentifiers);
new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
pubsubClient, accountLockManager, keys, messagesManager, profilesManager,
secureStorageClient, secureValueRecovery2Client, disconnectionRequestManager,