mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-19 21:38:05 +01:00
Write registration recovery passwords exclusively by PNI
This commit is contained in:
committed by
Jon Chambers
parent
8be43566a4
commit
2803c2acdb
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user