Registration recovery passwords store and manager
This commit is contained in:
		
							parent
							
								
									f5fec5e6bb
								
							
						
					
					
						commit
						8afe917a6c
					
				| 
						 | 
					@ -80,6 +80,9 @@ dynamoDbTables:
 | 
				
			||||||
    tableName: Example_ReservedUsernames
 | 
					    tableName: Example_ReservedUsernames
 | 
				
			||||||
  subscriptions:
 | 
					  subscriptions:
 | 
				
			||||||
    tableName: Example_Subscriptions
 | 
					    tableName: Example_Subscriptions
 | 
				
			||||||
 | 
					  registrationRecovery:
 | 
				
			||||||
 | 
					    tableName: Example_RegistrationRecovery
 | 
				
			||||||
 | 
					    expiration: P300D # Duration of time until rows expire
 | 
				
			||||||
 | 
					
 | 
				
			||||||
cacheCluster: # Redis server configuration for cache cluster
 | 
					cacheCluster: # Redis server configuration for cache cluster
 | 
				
			||||||
  configurationUri: redis://redis.example.com:6379/
 | 
					  configurationUri: redis://redis.example.com:6379/
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -197,6 +197,8 @@ import org.whispersystems.textsecuregcm.storage.PubSubManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.PushChallengeDynamoDb;
 | 
					import org.whispersystems.textsecuregcm.storage.PushChallengeDynamoDb;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.PushFeedbackProcessor;
 | 
					import org.whispersystems.textsecuregcm.storage.PushFeedbackProcessor;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager;
 | 
					import org.whispersystems.textsecuregcm.storage.RedeemedReceiptsManager;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
 | 
					import org.whispersystems.textsecuregcm.storage.RemoteConfigs;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
 | 
					import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
 | 
					import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
 | 
				
			||||||
| 
						 | 
					@ -368,6 +370,12 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
 | 
				
			||||||
        config.getDynamoDbTables().getPendingAccounts().getTableName());
 | 
					        config.getDynamoDbTables().getPendingAccounts().getTableName());
 | 
				
			||||||
    VerificationCodeStore pendingDevices = new VerificationCodeStore(dynamoDbClient,
 | 
					    VerificationCodeStore pendingDevices = new VerificationCodeStore(dynamoDbClient,
 | 
				
			||||||
        config.getDynamoDbTables().getPendingDevices().getTableName());
 | 
					        config.getDynamoDbTables().getPendingDevices().getTableName());
 | 
				
			||||||
 | 
					    RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
 | 
				
			||||||
 | 
					        config.getDynamoDbTables().getRegistrationRecovery().getTableName(),
 | 
				
			||||||
 | 
					        config.getDynamoDbTables().getRegistrationRecovery().getExpiration(),
 | 
				
			||||||
 | 
					        dynamoDbClient,
 | 
				
			||||||
 | 
					        dynamoDbAsyncClient
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    reactor.util.Metrics.MicrometerConfiguration.useRegistry(Metrics.globalRegistry);
 | 
					    reactor.util.Metrics.MicrometerConfiguration.useRegistry(Metrics.globalRegistry);
 | 
				
			||||||
    Schedulers.enableMetrics();
 | 
					    Schedulers.enableMetrics();
 | 
				
			||||||
| 
						 | 
					@ -464,6 +472,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
 | 
				
			||||||
    dynamicConfigurationManager.start();
 | 
					    dynamicConfigurationManager.start();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager);
 | 
					    ExperimentEnrollmentManager experimentEnrollmentManager = new ExperimentEnrollmentManager(dynamicConfigurationManager);
 | 
				
			||||||
 | 
					    RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    RegistrationServiceClient  registrationServiceClient  = new RegistrationServiceClient(config.getRegistrationServiceConfiguration().getHost(), config.getRegistrationServiceConfiguration().getPort(), config.getRegistrationServiceConfiguration().getApiKey(), config.getRegistrationServiceConfiguration().getRegistrationCaCertificate(), registrationCallbackExecutor);
 | 
					    RegistrationServiceClient  registrationServiceClient  = new RegistrationServiceClient(config.getRegistrationServiceConfiguration().getHost(), config.getRegistrationServiceConfiguration().getPort(), config.getRegistrationServiceConfiguration().getApiKey(), config.getRegistrationServiceConfiguration().getRegistrationCaCertificate(), registrationCallbackExecutor);
 | 
				
			||||||
    SecureBackupClient         secureBackupClient         = new SecureBackupClient(backupCredentialsGenerator, backupServiceExecutor, config.getSecureBackupServiceConfiguration());
 | 
					    SecureBackupClient         secureBackupClient         = new SecureBackupClient(backupCredentialsGenerator, backupServiceExecutor, config.getSecureBackupServiceConfiguration());
 | 
				
			||||||
| 
						 | 
					@ -485,7 +494,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
 | 
				
			||||||
    AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
 | 
					    AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
 | 
				
			||||||
        deletedAccountsManager, directoryQueue, keys, messagesManager, profilesManager,
 | 
					        deletedAccountsManager, directoryQueue, keys, messagesManager, profilesManager,
 | 
				
			||||||
        pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager,
 | 
					        pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager,
 | 
				
			||||||
        experimentEnrollmentManager, clock);
 | 
					        experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
 | 
				
			||||||
    RemoteConfigsManager       remoteConfigsManager       = new RemoteConfigsManager(remoteConfigs);
 | 
					    RemoteConfigsManager       remoteConfigsManager       = new RemoteConfigsManager(remoteConfigs);
 | 
				
			||||||
    DispatchManager            dispatchManager            = new DispatchManager(pubSubClientFactory, Optional.empty());
 | 
					    DispatchManager            dispatchManager            = new DispatchManager(pubSubClientFactory, Optional.empty());
 | 
				
			||||||
    PubSubManager              pubSubManager              = new PubSubManager(pubsubClient, dispatchManager);
 | 
					    PubSubManager              pubSubManager              = new PubSubManager(pubsubClient, dispatchManager);
 | 
				
			||||||
| 
						 | 
					@ -664,7 +673,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
 | 
				
			||||||
    environment.jersey().register(
 | 
					    environment.jersey().register(
 | 
				
			||||||
        new AccountController(pendingAccountsManager, accountsManager, rateLimiters,
 | 
					        new AccountController(pendingAccountsManager, accountsManager, rateLimiters,
 | 
				
			||||||
            registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
 | 
					            registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, config.getTestDevices(),
 | 
				
			||||||
            captchaChecker, pushNotificationManager, changeNumberManager, backupCredentialsGenerator,
 | 
					            captchaChecker, pushNotificationManager, changeNumberManager, registrationRecoveryPasswordsManager, backupCredentialsGenerator,
 | 
				
			||||||
            clientPresenceManager, clock));
 | 
					            clientPresenceManager, clock));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
 | 
					    environment.jersey().register(new KeysController(rateLimiters, keys, accountsManager));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,6 +62,7 @@ public class DynamoDbTables {
 | 
				
			||||||
  private final Table reportMessage;
 | 
					  private final Table reportMessage;
 | 
				
			||||||
  private final Table reservedUsernames;
 | 
					  private final Table reservedUsernames;
 | 
				
			||||||
  private final Table subscriptions;
 | 
					  private final Table subscriptions;
 | 
				
			||||||
 | 
					  private final TableWithExpiration registrationRecovery;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public DynamoDbTables(
 | 
					  public DynamoDbTables(
 | 
				
			||||||
      @JsonProperty("accounts") final AccountsTableConfiguration accounts,
 | 
					      @JsonProperty("accounts") final AccountsTableConfiguration accounts,
 | 
				
			||||||
| 
						 | 
					@ -79,7 +80,8 @@ public class DynamoDbTables {
 | 
				
			||||||
      @JsonProperty("remoteConfig") final Table remoteConfig,
 | 
					      @JsonProperty("remoteConfig") final Table remoteConfig,
 | 
				
			||||||
      @JsonProperty("reportMessage") final Table reportMessage,
 | 
					      @JsonProperty("reportMessage") final Table reportMessage,
 | 
				
			||||||
      @JsonProperty("reservedUsernames") final Table reservedUsernames,
 | 
					      @JsonProperty("reservedUsernames") final Table reservedUsernames,
 | 
				
			||||||
      @JsonProperty("subscriptions") final Table subscriptions) {
 | 
					      @JsonProperty("subscriptions") final Table subscriptions,
 | 
				
			||||||
 | 
					      @JsonProperty("registrationRecovery") final TableWithExpiration registrationRecovery) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.accounts = accounts;
 | 
					    this.accounts = accounts;
 | 
				
			||||||
    this.deletedAccounts = deletedAccounts;
 | 
					    this.deletedAccounts = deletedAccounts;
 | 
				
			||||||
| 
						 | 
					@ -97,6 +99,7 @@ public class DynamoDbTables {
 | 
				
			||||||
    this.reportMessage = reportMessage;
 | 
					    this.reportMessage = reportMessage;
 | 
				
			||||||
    this.reservedUsernames = reservedUsernames;
 | 
					    this.reservedUsernames = reservedUsernames;
 | 
				
			||||||
    this.subscriptions = subscriptions;
 | 
					    this.subscriptions = subscriptions;
 | 
				
			||||||
 | 
					    this.registrationRecovery = registrationRecovery;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @NotNull
 | 
					  @NotNull
 | 
				
			||||||
| 
						 | 
					@ -194,4 +197,10 @@ public class DynamoDbTables {
 | 
				
			||||||
  public Table getSubscriptions() {
 | 
					  public Table getSubscriptions() {
 | 
				
			||||||
    return subscriptions;
 | 
					    return subscriptions;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @NotNull
 | 
				
			||||||
 | 
					  @Valid
 | 
				
			||||||
 | 
					  public TableWithExpiration getRegistrationRecovery() {
 | 
				
			||||||
 | 
					    return registrationRecovery;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -85,8 +85,8 @@ import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
 | 
					import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
 | 
					import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
 | 
					import org.whispersystems.textsecuregcm.entities.StaleDevices;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
 | 
					import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
 | 
				
			||||||
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.metrics.UserAgentTagUtil;
 | 
					import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
 | 
				
			||||||
| 
						 | 
					@ -102,6 +102,7 @@ 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.DynamicConfigurationManager;
 | 
					import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
 | 
					import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException;
 | 
					import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
 | 
					import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
 | 
				
			||||||
| 
						 | 
					@ -163,7 +164,7 @@ public class AccountController {
 | 
				
			||||||
  private final CaptchaChecker                     captchaChecker;
 | 
					  private final CaptchaChecker                     captchaChecker;
 | 
				
			||||||
  private final PushNotificationManager            pushNotificationManager;
 | 
					  private final PushNotificationManager            pushNotificationManager;
 | 
				
			||||||
  private final ExternalServiceCredentialsGenerator backupServiceCredentialsGenerator;
 | 
					  private final ExternalServiceCredentialsGenerator backupServiceCredentialsGenerator;
 | 
				
			||||||
 | 
					  private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
 | 
				
			||||||
  private final ChangeNumberManager changeNumberManager;
 | 
					  private final ChangeNumberManager changeNumberManager;
 | 
				
			||||||
  private final Clock clock;
 | 
					  private final Clock clock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -183,6 +184,7 @@ public class AccountController {
 | 
				
			||||||
      CaptchaChecker captchaChecker,
 | 
					      CaptchaChecker captchaChecker,
 | 
				
			||||||
      PushNotificationManager pushNotificationManager,
 | 
					      PushNotificationManager pushNotificationManager,
 | 
				
			||||||
      ChangeNumberManager changeNumberManager,
 | 
					      ChangeNumberManager changeNumberManager,
 | 
				
			||||||
 | 
					      RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
 | 
				
			||||||
      ExternalServiceCredentialsGenerator backupServiceCredentialsGenerator,
 | 
					      ExternalServiceCredentialsGenerator backupServiceCredentialsGenerator,
 | 
				
			||||||
      ClientPresenceManager clientPresenceManager,
 | 
					      ClientPresenceManager clientPresenceManager,
 | 
				
			||||||
      Clock clock
 | 
					      Clock clock
 | 
				
			||||||
| 
						 | 
					@ -199,6 +201,7 @@ public class AccountController {
 | 
				
			||||||
    this.backupServiceCredentialsGenerator = backupServiceCredentialsGenerator;
 | 
					    this.backupServiceCredentialsGenerator = backupServiceCredentialsGenerator;
 | 
				
			||||||
    this.changeNumberManager = changeNumberManager;
 | 
					    this.changeNumberManager = changeNumberManager;
 | 
				
			||||||
    this.clientPresenceManager = clientPresenceManager;
 | 
					    this.clientPresenceManager = clientPresenceManager;
 | 
				
			||||||
 | 
					    this.registrationRecoveryPasswordsManager = registrationRecoveryPasswordsManager;
 | 
				
			||||||
    this.clock = clock;
 | 
					    this.clock = clock;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -214,11 +217,12 @@ public class AccountController {
 | 
				
			||||||
      CaptchaChecker captchaChecker,
 | 
					      CaptchaChecker captchaChecker,
 | 
				
			||||||
      PushNotificationManager pushNotificationManager,
 | 
					      PushNotificationManager pushNotificationManager,
 | 
				
			||||||
      ChangeNumberManager changeNumberManager,
 | 
					      ChangeNumberManager changeNumberManager,
 | 
				
			||||||
 | 
					      RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
 | 
				
			||||||
      ExternalServiceCredentialsGenerator backupServiceCredentialsGenerator
 | 
					      ExternalServiceCredentialsGenerator backupServiceCredentialsGenerator
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    this(pendingAccounts, accounts, rateLimiters,
 | 
					    this(pendingAccounts, accounts, rateLimiters,
 | 
				
			||||||
        registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, testDevices, captchaChecker,
 | 
					        registrationServiceClient, dynamicConfigurationManager, turnTokenGenerator, testDevices, captchaChecker,
 | 
				
			||||||
        pushNotificationManager, changeNumberManager,
 | 
					        pushNotificationManager, changeNumberManager, registrationRecoveryPasswordsManager,
 | 
				
			||||||
        backupServiceCredentialsGenerator, null, Clock.systemUTC());
 | 
					        backupServiceCredentialsGenerator, null, Clock.systemUTC());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -645,13 +649,14 @@ public class AccountController {
 | 
				
			||||||
  @Consumes(MediaType.APPLICATION_JSON)
 | 
					  @Consumes(MediaType.APPLICATION_JSON)
 | 
				
			||||||
  @Produces(MediaType.APPLICATION_JSON)
 | 
					  @Produces(MediaType.APPLICATION_JSON)
 | 
				
			||||||
  @ChangesDeviceEnabledState
 | 
					  @ChangesDeviceEnabledState
 | 
				
			||||||
  public void setAccountAttributes(@Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
 | 
					  public void setAccountAttributes(
 | 
				
			||||||
 | 
					      @Auth DisabledPermittedAuthenticatedAccount disabledPermittedAuth,
 | 
				
			||||||
      @HeaderParam(HeaderUtils.X_SIGNAL_AGENT) String userAgent,
 | 
					      @HeaderParam(HeaderUtils.X_SIGNAL_AGENT) String userAgent,
 | 
				
			||||||
      @NotNull @Valid AccountAttributes attributes) {
 | 
					      @NotNull @Valid AccountAttributes attributes) {
 | 
				
			||||||
    Account account = disabledPermittedAuth.getAccount();
 | 
					    final Account account = disabledPermittedAuth.getAccount();
 | 
				
			||||||
    long deviceId = disabledPermittedAuth.getAuthenticatedDevice().getId();
 | 
					    final long deviceId = disabledPermittedAuth.getAuthenticatedDevice().getId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    accounts.update(account, a -> {
 | 
					    final Account updatedAccount = accounts.update(account, a -> {
 | 
				
			||||||
      a.getDevice(deviceId).ifPresent(d -> {
 | 
					      a.getDevice(deviceId).ifPresent(d -> {
 | 
				
			||||||
        d.setFetchesMessages(attributes.getFetchesMessages());
 | 
					        d.setFetchesMessages(attributes.getFetchesMessages());
 | 
				
			||||||
        d.setName(attributes.getName());
 | 
					        d.setName(attributes.getName());
 | 
				
			||||||
| 
						 | 
					@ -667,6 +672,10 @@ public class AccountController {
 | 
				
			||||||
      a.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
 | 
					      a.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
 | 
				
			||||||
      a.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber());
 | 
					      a.setDiscoverableByPhoneNumber(attributes.isDiscoverableByPhoneNumber());
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // if registration recovery password was sent to us, store it (or refresh its expiration)
 | 
				
			||||||
 | 
					    attributes.recoveryPassword().ifPresent(registrationRecoveryPassword ->
 | 
				
			||||||
 | 
					        registrationRecoveryPasswordsManager.storeForCurrentNumber(updatedAccount.getNumber(), registrationRecoveryPassword));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @GET
 | 
					  @GET
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,19 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 * Copyright 2013-2020 Signal Messenger, LLC
 | 
					 * Copyright 2013 Signal Messenger, LLC
 | 
				
			||||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
package org.whispersystems.textsecuregcm.entities;
 | 
					package org.whispersystems.textsecuregcm.entities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import com.fasterxml.jackson.annotation.JsonProperty;
 | 
					import com.fasterxml.jackson.annotation.JsonProperty;
 | 
				
			||||||
 | 
					import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
 | 
				
			||||||
import com.google.common.annotations.VisibleForTesting;
 | 
					import com.google.common.annotations.VisibleForTesting;
 | 
				
			||||||
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					import java.util.OptionalInt;
 | 
				
			||||||
import javax.annotation.Nullable;
 | 
					import javax.annotation.Nullable;
 | 
				
			||||||
import javax.validation.constraints.Size;
 | 
					import javax.validation.constraints.Size;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
 | 
					import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
 | 
					import org.whispersystems.textsecuregcm.util.ExactlySize;
 | 
				
			||||||
import java.util.OptionalInt;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class AccountAttributes {
 | 
					public class AccountAttributes {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +23,6 @@ public class AccountAttributes {
 | 
				
			||||||
  @JsonProperty
 | 
					  @JsonProperty
 | 
				
			||||||
  private int registrationId;
 | 
					  private int registrationId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Nullable
 | 
					 | 
				
			||||||
  @JsonProperty("pniRegistrationId")
 | 
					  @JsonProperty("pniRegistrationId")
 | 
				
			||||||
  private Integer phoneNumberIdentityRegistrationId;
 | 
					  private Integer phoneNumberIdentityRegistrationId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,11 +46,22 @@ public class AccountAttributes {
 | 
				
			||||||
  @JsonProperty
 | 
					  @JsonProperty
 | 
				
			||||||
  private boolean discoverableByPhoneNumber = true;
 | 
					  private boolean discoverableByPhoneNumber = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public AccountAttributes() {}
 | 
					  @JsonProperty
 | 
				
			||||||
 | 
					  @Nullable
 | 
				
			||||||
 | 
					  @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
 | 
				
			||||||
 | 
					  private byte[] recoveryPassword = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public AccountAttributes() {
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @VisibleForTesting
 | 
					  @VisibleForTesting
 | 
				
			||||||
  public AccountAttributes(boolean fetchesMessages, int registrationId, String name, String registrationLock,
 | 
					  public AccountAttributes(
 | 
				
			||||||
      boolean discoverableByPhoneNumber, final DeviceCapabilities capabilities) {
 | 
					      final boolean fetchesMessages,
 | 
				
			||||||
 | 
					      final int registrationId,
 | 
				
			||||||
 | 
					      final String name,
 | 
				
			||||||
 | 
					      final String registrationLock,
 | 
				
			||||||
 | 
					      final boolean discoverableByPhoneNumber,
 | 
				
			||||||
 | 
					      final DeviceCapabilities capabilities) {
 | 
				
			||||||
    this.fetchesMessages = fetchesMessages;
 | 
					    this.fetchesMessages = fetchesMessages;
 | 
				
			||||||
    this.registrationId = registrationId;
 | 
					    this.registrationId = registrationId;
 | 
				
			||||||
    this.name = name;
 | 
					    this.name = name;
 | 
				
			||||||
| 
						 | 
					@ -93,8 +106,19 @@ public class AccountAttributes {
 | 
				
			||||||
    return discoverableByPhoneNumber;
 | 
					    return discoverableByPhoneNumber;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public Optional<byte[]> recoveryPassword() {
 | 
				
			||||||
 | 
					    return Optional.ofNullable(recoveryPassword);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @VisibleForTesting
 | 
					  @VisibleForTesting
 | 
				
			||||||
  public void setUnidentifiedAccessKey(final byte[] unidentifiedAccessKey) {
 | 
					  public AccountAttributes withUnidentifiedAccessKey(final byte[] unidentifiedAccessKey) {
 | 
				
			||||||
    this.unidentifiedAccessKey = unidentifiedAccessKey;
 | 
					    this.unidentifiedAccessKey = unidentifiedAccessKey;
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @VisibleForTesting
 | 
				
			||||||
 | 
					  public AccountAttributes withRecoveryPassword(final byte[] recoveryPassword) {
 | 
				
			||||||
 | 
					    this.recoveryPassword = recoveryPassword;
 | 
				
			||||||
 | 
					    return this;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ package org.whispersystems.textsecuregcm.storage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import static com.codahale.metrics.MetricRegistry.name;
 | 
					import static com.codahale.metrics.MetricRegistry.name;
 | 
				
			||||||
 | 
					import static java.util.Objects.requireNonNull;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import com.codahale.metrics.MetricRegistry;
 | 
					import com.codahale.metrics.MetricRegistry;
 | 
				
			||||||
import com.codahale.metrics.SharedMetricRegistries;
 | 
					import com.codahale.metrics.SharedMetricRegistries;
 | 
				
			||||||
| 
						 | 
					@ -26,7 +27,6 @@ import java.util.Base64;
 | 
				
			||||||
import java.util.Collections;
 | 
					import java.util.Collections;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
import java.util.Map;
 | 
					import java.util.Map;
 | 
				
			||||||
import java.util.Objects;
 | 
					 | 
				
			||||||
import java.util.Optional;
 | 
					import java.util.Optional;
 | 
				
			||||||
import java.util.UUID;
 | 
					import java.util.UUID;
 | 
				
			||||||
import java.util.concurrent.CompletableFuture;
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
| 
						 | 
					@ -93,6 +93,7 @@ public class AccountsManager {
 | 
				
			||||||
  private final SecureBackupClient secureBackupClient;
 | 
					  private final SecureBackupClient secureBackupClient;
 | 
				
			||||||
  private final ClientPresenceManager clientPresenceManager;
 | 
					  private final ClientPresenceManager clientPresenceManager;
 | 
				
			||||||
  private final ExperimentEnrollmentManager experimentEnrollmentManager;
 | 
					  private final ExperimentEnrollmentManager experimentEnrollmentManager;
 | 
				
			||||||
 | 
					  private final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
 | 
				
			||||||
  private final Clock clock;
 | 
					  private final Clock clock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static final ObjectMapper mapper = SystemMapper.getMapper();
 | 
					  private static final ObjectMapper mapper = SystemMapper.getMapper();
 | 
				
			||||||
| 
						 | 
					@ -135,6 +136,7 @@ public class AccountsManager {
 | 
				
			||||||
      final SecureBackupClient secureBackupClient,
 | 
					      final SecureBackupClient secureBackupClient,
 | 
				
			||||||
      final ClientPresenceManager clientPresenceManager,
 | 
					      final ClientPresenceManager clientPresenceManager,
 | 
				
			||||||
      final ExperimentEnrollmentManager experimentEnrollmentManager,
 | 
					      final ExperimentEnrollmentManager experimentEnrollmentManager,
 | 
				
			||||||
 | 
					      final RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager,
 | 
				
			||||||
      final Clock clock) {
 | 
					      final Clock clock) {
 | 
				
			||||||
    this.accounts = accounts;
 | 
					    this.accounts = accounts;
 | 
				
			||||||
    this.phoneNumberIdentifiers = phoneNumberIdentifiers;
 | 
					    this.phoneNumberIdentifiers = phoneNumberIdentifiers;
 | 
				
			||||||
| 
						 | 
					@ -149,7 +151,8 @@ public class AccountsManager {
 | 
				
			||||||
    this.secureBackupClient  = secureBackupClient;
 | 
					    this.secureBackupClient  = secureBackupClient;
 | 
				
			||||||
    this.clientPresenceManager = clientPresenceManager;
 | 
					    this.clientPresenceManager = clientPresenceManager;
 | 
				
			||||||
    this.experimentEnrollmentManager = experimentEnrollmentManager;
 | 
					    this.experimentEnrollmentManager = experimentEnrollmentManager;
 | 
				
			||||||
    this.clock = Objects.requireNonNull(clock);
 | 
					    this.registrationRecoveryPasswordsManager = requireNonNull(registrationRecoveryPasswordsManager);
 | 
				
			||||||
 | 
					    this.clock = requireNonNull(clock);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public Account create(final String number,
 | 
					  public Account create(final String number,
 | 
				
			||||||
| 
						 | 
					@ -230,6 +233,9 @@ public class AccountsManager {
 | 
				
			||||||
          // The newly-created account has explicitly opted out of discoverability
 | 
					          // The newly-created account has explicitly opted out of discoverability
 | 
				
			||||||
          directoryQueue.deleteAccount(account);
 | 
					          directoryQueue.deleteAccount(account);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        accountAttributes.recoveryPassword().ifPresent(registrationRecoveryPassword ->
 | 
				
			||||||
 | 
					            registrationRecoveryPasswordsManager.storeForCurrentNumber(account.getNumber(), registrationRecoveryPassword));
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return account;
 | 
					      return account;
 | 
				
			||||||
| 
						 | 
					@ -451,12 +457,7 @@ public class AccountsManager {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public Account updateDeviceAuthentication(final Account account, final Device device, final SaltedTokenHash credentials) {
 | 
					  public Account updateDeviceAuthentication(final Account account, final Device device, final SaltedTokenHash credentials) {
 | 
				
			||||||
    Preconditions.checkArgument(credentials.getVersion() == SaltedTokenHash.CURRENT_VERSION);
 | 
					    Preconditions.checkArgument(credentials.getVersion() == SaltedTokenHash.CURRENT_VERSION);
 | 
				
			||||||
    return updateDevice(account, device.getId(), new Consumer<Device>() {
 | 
					    return updateDevice(account, device.getId(), device1 -> device1.setAuthTokenHash(credentials));
 | 
				
			||||||
      @Override
 | 
					 | 
				
			||||||
      public void accept(final Device device) {
 | 
					 | 
				
			||||||
        device.setAuthTokenHash(credentials);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
| 
						 | 
					@ -662,6 +663,7 @@ public class AccountsManager {
 | 
				
			||||||
    keys.delete(account.getPhoneNumberIdentifier());
 | 
					    keys.delete(account.getPhoneNumberIdentifier());
 | 
				
			||||||
    messagesManager.clear(account.getUuid());
 | 
					    messagesManager.clear(account.getUuid());
 | 
				
			||||||
    messagesManager.clear(account.getPhoneNumberIdentifier());
 | 
					    messagesManager.clear(account.getPhoneNumberIdentifier());
 | 
				
			||||||
 | 
					    registrationRecoveryPasswordsManager.removeForNumber(account.getNumber());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    deleteStorageServiceDataFuture.join();
 | 
					    deleteStorageServiceDataFuture.join();
 | 
				
			||||||
    deleteBackupServiceDataFuture.join();
 | 
					    deleteBackupServiceDataFuture.join();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,101 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright 2023 Signal Messenger, LLC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package org.whispersystems.textsecuregcm.storage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import static java.util.Objects.requireNonNull;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.time.Clock;
 | 
				
			||||||
 | 
					import java.time.Duration;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					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.GetItemRequest;
 | 
				
			||||||
 | 
					import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class RegistrationRecoveryPasswords extends AbstractDynamoDbStore {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static final String KEY_E164 = "P";
 | 
				
			||||||
 | 
					  static final String ATTR_EXP = "E";
 | 
				
			||||||
 | 
					  static final String ATTR_SALT = "S";
 | 
				
			||||||
 | 
					  static final String ATTR_HASH = "H";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private final String tableName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private final Duration expiration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private final DynamoDbAsyncClient asyncClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private final Clock clock;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public RegistrationRecoveryPasswords(
 | 
				
			||||||
 | 
					      final String tableName,
 | 
				
			||||||
 | 
					      final Duration expiration,
 | 
				
			||||||
 | 
					      final DynamoDbClient dynamoDbClient,
 | 
				
			||||||
 | 
					      final DynamoDbAsyncClient asyncClient) {
 | 
				
			||||||
 | 
					    this(tableName, expiration, dynamoDbClient, asyncClient, Clock.systemUTC());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  RegistrationRecoveryPasswords(
 | 
				
			||||||
 | 
					      final String tableName,
 | 
				
			||||||
 | 
					      final Duration expiration,
 | 
				
			||||||
 | 
					      final DynamoDbClient dynamoDbClient,
 | 
				
			||||||
 | 
					      final DynamoDbAsyncClient asyncClient,
 | 
				
			||||||
 | 
					      final Clock clock) {
 | 
				
			||||||
 | 
					    super(dynamoDbClient);
 | 
				
			||||||
 | 
					    this.tableName = requireNonNull(tableName);
 | 
				
			||||||
 | 
					    this.expiration = requireNonNull(expiration);
 | 
				
			||||||
 | 
					    this.asyncClient = requireNonNull(asyncClient);
 | 
				
			||||||
 | 
					    this.clock = requireNonNull(clock);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public CompletableFuture<Optional<SaltedTokenHash>> lookup(final String number) {
 | 
				
			||||||
 | 
					    return asyncClient.getItem(GetItemRequest.builder()
 | 
				
			||||||
 | 
					        .tableName(tableName)
 | 
				
			||||||
 | 
					        .key(Map.of(
 | 
				
			||||||
 | 
					            KEY_E164, AttributeValues.fromString(number)))
 | 
				
			||||||
 | 
					        .build())
 | 
				
			||||||
 | 
					        .thenApply(getItemResponse -> {
 | 
				
			||||||
 | 
					          final Map<String, AttributeValue> item = getItemResponse.item();
 | 
				
			||||||
 | 
					          if (item == null || !item.containsKey(ATTR_SALT) || !item.containsKey(ATTR_HASH)) {
 | 
				
			||||||
 | 
					            return Optional.empty();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          final String salt = item.get(ATTR_SALT).s();
 | 
				
			||||||
 | 
					          final String hash = item.get(ATTR_HASH).s();
 | 
				
			||||||
 | 
					          return Optional.of(new SaltedTokenHash(hash, salt));
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public CompletableFuture<Void> addOrReplace(final String number, final SaltedTokenHash data) {
 | 
				
			||||||
 | 
					    return asyncClient.putItem(PutItemRequest.builder()
 | 
				
			||||||
 | 
					            .tableName(tableName)
 | 
				
			||||||
 | 
					            .item(Map.of(
 | 
				
			||||||
 | 
					                KEY_E164, AttributeValues.fromString(number),
 | 
				
			||||||
 | 
					                ATTR_EXP, AttributeValues.fromLong(expirationSeconds()),
 | 
				
			||||||
 | 
					                ATTR_SALT, AttributeValues.fromString(data.salt()),
 | 
				
			||||||
 | 
					                ATTR_HASH, AttributeValues.fromString(data.hash())))
 | 
				
			||||||
 | 
					            .build())
 | 
				
			||||||
 | 
					        .thenRun(Util.NOOP);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public CompletableFuture<Void> removeEntry(final String number) {
 | 
				
			||||||
 | 
					    return asyncClient.deleteItem(DeleteItemRequest.builder()
 | 
				
			||||||
 | 
					            .tableName(tableName)
 | 
				
			||||||
 | 
					            .key(Map.of(KEY_E164, AttributeValues.fromString(number)))
 | 
				
			||||||
 | 
					            .build())
 | 
				
			||||||
 | 
					        .thenRun(Util.NOOP);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private long expirationSeconds() {
 | 
				
			||||||
 | 
					    return clock.instant().plus(expiration).getEpochSecond();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,65 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright 2023 Signal Messenger, LLC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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.concurrent.CompletableFuture;
 | 
				
			||||||
 | 
					import org.slf4j.Logger;
 | 
				
			||||||
 | 
					import org.slf4j.LoggerFactory;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 String number, final byte[] password) {
 | 
				
			||||||
 | 
					    return registrationRecoveryPasswords.lookup(number)
 | 
				
			||||||
 | 
					        .thenApply(maybeHash -> maybeHash.filter(hash -> hash.verify(bytesToString(password))))
 | 
				
			||||||
 | 
					        .whenComplete((result, error) -> {
 | 
				
			||||||
 | 
					          if (error != null) {
 | 
				
			||||||
 | 
					            logger.warn("Failed to lookup Registration Recovery Password", error);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .thenApply(Optional::isPresent);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public CompletableFuture<Void> storeForCurrentNumber(final String number, final byte[] password) {
 | 
				
			||||||
 | 
					    final String token = bytesToString(password);
 | 
				
			||||||
 | 
					    final SaltedTokenHash tokenHash = SaltedTokenHash.generateFor(token);
 | 
				
			||||||
 | 
					    return registrationRecoveryPasswords.addOrReplace(number, tokenHash)
 | 
				
			||||||
 | 
					        .whenComplete((result, error) -> {
 | 
				
			||||||
 | 
					          if (error != null) {
 | 
				
			||||||
 | 
					            logger.warn("Failed to store Registration Recovery Password", error);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public CompletableFuture<Void> removeForNumber(final String number) {
 | 
				
			||||||
 | 
					    // remove is a "fire-and-forget" operation,
 | 
				
			||||||
 | 
					    // there is no action to be taken on its completion
 | 
				
			||||||
 | 
					    return registrationRecoveryPasswords.removeEntry(number)
 | 
				
			||||||
 | 
					        .whenComplete((ignored, error) -> {
 | 
				
			||||||
 | 
					          if (error != null) {
 | 
				
			||||||
 | 
					            logger.warn("Failed to remove Registration Recovery Password", error);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static String bytesToString(final byte[] bytes) {
 | 
				
			||||||
 | 
					    return HexFormat.of().formatHex(bytes);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 * Copyright 2013-2020 Signal Messenger, LLC
 | 
					 * Copyright 2013 Signal Messenger, LLC
 | 
				
			||||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
package org.whispersystems.textsecuregcm.util;
 | 
					package org.whispersystems.textsecuregcm.util;
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,6 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
 | 
				
			||||||
import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
 | 
					import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
 | 
				
			||||||
import java.time.Clock;
 | 
					import java.time.Clock;
 | 
				
			||||||
import java.time.Duration;
 | 
					import java.time.Duration;
 | 
				
			||||||
import java.time.temporal.ChronoField;
 | 
					 | 
				
			||||||
import java.util.Arrays;
 | 
					import java.util.Arrays;
 | 
				
			||||||
import java.util.Collection;
 | 
					import java.util.Collection;
 | 
				
			||||||
import java.util.List;
 | 
					import java.util.List;
 | 
				
			||||||
| 
						 | 
					@ -29,6 +28,8 @@ public class Util {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static final PhoneNumberUtil PHONE_NUMBER_UTIL = PhoneNumberUtil.getInstance();
 | 
					  private static final PhoneNumberUtil PHONE_NUMBER_UTIL = PhoneNumberUtil.getInstance();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public static final Runnable NOOP = () -> {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Checks that the given number is a valid, E164-normalized phone number.
 | 
					   * Checks that the given number is a valid, E164-normalized phone number.
 | 
				
			||||||
   *
 | 
					   *
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,6 +49,8 @@ import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Profiles;
 | 
					import org.whispersystems.textsecuregcm.storage.Profiles;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
 | 
					import org.whispersystems.textsecuregcm.storage.ProfilesManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ProhibitedUsernames;
 | 
					import org.whispersystems.textsecuregcm.storage.ProhibitedUsernames;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
 | 
					import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
 | 
					import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
 | 
					import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
 | 
				
			||||||
| 
						 | 
					@ -144,6 +146,14 @@ public class AssignUsernameCommand extends EnvironmentCommand<WhisperServerConfi
 | 
				
			||||||
        configuration.getDynamoDbTables().getDeletedAccounts().getNeedsReconciliationIndexName());
 | 
					        configuration.getDynamoDbTables().getDeletedAccounts().getNeedsReconciliationIndexName());
 | 
				
			||||||
    VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
 | 
					    VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
 | 
				
			||||||
        configuration.getDynamoDbTables().getPendingAccounts().getTableName());
 | 
					        configuration.getDynamoDbTables().getPendingAccounts().getTableName());
 | 
				
			||||||
 | 
					    RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
 | 
				
			||||||
 | 
					        configuration.getDynamoDbTables().getRegistrationRecovery().getTableName(),
 | 
				
			||||||
 | 
					        configuration.getDynamoDbTables().getRegistrationRecovery().getExpiration(),
 | 
				
			||||||
 | 
					        dynamoDbClient,
 | 
				
			||||||
 | 
					        dynamoDbAsyncClient
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Accounts accounts = new Accounts(
 | 
					    Accounts accounts = new Accounts(
 | 
				
			||||||
        dynamoDbClient,
 | 
					        dynamoDbClient,
 | 
				
			||||||
| 
						 | 
					@ -198,7 +208,7 @@ public class AssignUsernameCommand extends EnvironmentCommand<WhisperServerConfi
 | 
				
			||||||
    AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
 | 
					    AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
 | 
				
			||||||
        deletedAccountsManager, directoryQueue, keys, messagesManager, profilesManager,
 | 
					        deletedAccountsManager, directoryQueue, keys, messagesManager, profilesManager,
 | 
				
			||||||
        pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager,
 | 
					        pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager,
 | 
				
			||||||
        experimentEnrollmentManager, Clock.systemUTC());
 | 
					        experimentEnrollmentManager, registrationRecoveryPasswordsManager, Clock.systemUTC());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final String usernameHash = namespace.getString("usernameHash");
 | 
					    final String usernameHash = namespace.getString("usernameHash");
 | 
				
			||||||
    final UUID accountIdentifier = UUID.fromString(namespace.getString("aci"));
 | 
					    final UUID accountIdentifier = UUID.fromString(namespace.getString("aci"));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,6 +50,8 @@ import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Profiles;
 | 
					import org.whispersystems.textsecuregcm.storage.Profiles;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
 | 
					import org.whispersystems.textsecuregcm.storage.ProfilesManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ProhibitedUsernames;
 | 
					import org.whispersystems.textsecuregcm.storage.ProhibitedUsernames;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
 | 
					import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
 | 
					import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
 | 
					import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
 | 
				
			||||||
| 
						 | 
					@ -143,6 +145,14 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
 | 
				
			||||||
          configuration.getDynamoDbTables().getDeletedAccounts().getNeedsReconciliationIndexName());
 | 
					          configuration.getDynamoDbTables().getDeletedAccounts().getNeedsReconciliationIndexName());
 | 
				
			||||||
      VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
 | 
					      VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
 | 
				
			||||||
          configuration.getDynamoDbTables().getPendingAccounts().getTableName());
 | 
					          configuration.getDynamoDbTables().getPendingAccounts().getTableName());
 | 
				
			||||||
 | 
					      RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
 | 
				
			||||||
 | 
					          configuration.getDynamoDbTables().getRegistrationRecovery().getTableName(),
 | 
				
			||||||
 | 
					          configuration.getDynamoDbTables().getRegistrationRecovery().getExpiration(),
 | 
				
			||||||
 | 
					          dynamoDbClient,
 | 
				
			||||||
 | 
					          dynamoDbAsyncClient
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Accounts accounts = new Accounts(
 | 
					      Accounts accounts = new Accounts(
 | 
				
			||||||
          dynamoDbClient,
 | 
					          dynamoDbClient,
 | 
				
			||||||
| 
						 | 
					@ -197,7 +207,7 @@ public class DeleteUserCommand extends EnvironmentCommand<WhisperServerConfigura
 | 
				
			||||||
      AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
 | 
					      AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
 | 
				
			||||||
          deletedAccountsManager, directoryQueue, keys, messagesManager, profilesManager,
 | 
					          deletedAccountsManager, directoryQueue, keys, messagesManager, profilesManager,
 | 
				
			||||||
          pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager,
 | 
					          pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager,
 | 
				
			||||||
          experimentEnrollmentManager, clock);
 | 
					          experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (String user : users) {
 | 
					      for (String user : users) {
 | 
				
			||||||
        Optional<Account> account = accountsManager.getByE164(user);
 | 
					        Optional<Account> account = accountsManager.getByE164(user);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,6 +48,8 @@ import org.whispersystems.textsecuregcm.storage.PhoneNumberIdentifiers;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Profiles;
 | 
					import org.whispersystems.textsecuregcm.storage.Profiles;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ProfilesManager;
 | 
					import org.whispersystems.textsecuregcm.storage.ProfilesManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ProhibitedUsernames;
 | 
					import org.whispersystems.textsecuregcm.storage.ProhibitedUsernames;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswords;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
 | 
					import org.whispersystems.textsecuregcm.storage.ReportMessageDynamoDb;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
 | 
					import org.whispersystems.textsecuregcm.storage.ReportMessageManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
 | 
					import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
 | 
				
			||||||
| 
						 | 
					@ -146,6 +148,14 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
 | 
				
			||||||
          configuration.getDynamoDbTables().getDeletedAccounts().getNeedsReconciliationIndexName());
 | 
					          configuration.getDynamoDbTables().getDeletedAccounts().getNeedsReconciliationIndexName());
 | 
				
			||||||
      VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
 | 
					      VerificationCodeStore pendingAccounts = new VerificationCodeStore(dynamoDbClient,
 | 
				
			||||||
          configuration.getDynamoDbTables().getPendingAccounts().getTableName());
 | 
					          configuration.getDynamoDbTables().getPendingAccounts().getTableName());
 | 
				
			||||||
 | 
					      RegistrationRecoveryPasswords registrationRecoveryPasswords = new RegistrationRecoveryPasswords(
 | 
				
			||||||
 | 
					          configuration.getDynamoDbTables().getRegistrationRecovery().getTableName(),
 | 
				
			||||||
 | 
					          configuration.getDynamoDbTables().getRegistrationRecovery().getExpiration(),
 | 
				
			||||||
 | 
					          dynamoDbClient,
 | 
				
			||||||
 | 
					          dynamoDbAsyncClient
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = new RegistrationRecoveryPasswordsManager(registrationRecoveryPasswords);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Accounts accounts = new Accounts(
 | 
					      Accounts accounts = new Accounts(
 | 
				
			||||||
          dynamoDbClient,
 | 
					          dynamoDbClient,
 | 
				
			||||||
| 
						 | 
					@ -198,7 +208,7 @@ public class SetUserDiscoverabilityCommand extends EnvironmentCommand<WhisperSer
 | 
				
			||||||
      AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
 | 
					      AccountsManager accountsManager = new AccountsManager(accounts, phoneNumberIdentifiers, cacheCluster,
 | 
				
			||||||
          deletedAccountsManager, directoryQueue, keys, messagesManager, profilesManager,
 | 
					          deletedAccountsManager, directoryQueue, keys, messagesManager, profilesManager,
 | 
				
			||||||
          pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager,
 | 
					          pendingAccountsManager, secureStorageClient, secureBackupClient, clientPresenceManager,
 | 
				
			||||||
          experimentEnrollmentManager, clock);
 | 
					          experimentEnrollmentManager, registrationRecoveryPasswordsManager, clock);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Optional<Account> maybeAccount;
 | 
					      Optional<Account> maybeAccount;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,6 +50,7 @@ import javax.annotation.Nullable;
 | 
				
			||||||
import javax.ws.rs.client.Entity;
 | 
					import javax.ws.rs.client.Entity;
 | 
				
			||||||
import javax.ws.rs.core.MediaType;
 | 
					import javax.ws.rs.core.MediaType;
 | 
				
			||||||
import javax.ws.rs.core.Response;
 | 
					import javax.ws.rs.core.Response;
 | 
				
			||||||
 | 
					import org.apache.commons.lang3.RandomUtils;
 | 
				
			||||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
 | 
					import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
 | 
				
			||||||
import org.junit.jupiter.api.AfterEach;
 | 
					import org.junit.jupiter.api.AfterEach;
 | 
				
			||||||
import org.junit.jupiter.api.BeforeEach;
 | 
					import org.junit.jupiter.api.BeforeEach;
 | 
				
			||||||
| 
						 | 
					@ -87,8 +88,8 @@ import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
 | 
					import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
 | 
					import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
 | 
					import org.whispersystems.textsecuregcm.entities.SignedPreKey;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.limits.RateLimitByIpFilter;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
 | 
					import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.limits.RateLimitByIpFilter;
 | 
				
			||||||
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.mappers.ImpossiblePhoneNumberExceptionMapper;
 | 
					import org.whispersystems.textsecuregcm.mappers.ImpossiblePhoneNumberExceptionMapper;
 | 
				
			||||||
| 
						 | 
					@ -107,6 +108,7 @@ 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.DynamicConfigurationManager;
 | 
					import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
 | 
					import org.whispersystems.textsecuregcm.storage.StoredVerificationCodeManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException;
 | 
					import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
 | 
					import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
 | 
				
			||||||
| 
						 | 
					@ -173,6 +175,8 @@ class AccountControllerTest {
 | 
				
			||||||
  private static CaptchaChecker captchaChecker = mock(CaptchaChecker.class);
 | 
					  private static CaptchaChecker captchaChecker = mock(CaptchaChecker.class);
 | 
				
			||||||
  private static PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class);
 | 
					  private static PushNotificationManager pushNotificationManager = mock(PushNotificationManager.class);
 | 
				
			||||||
  private static ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class);
 | 
					  private static ChangeNumberManager changeNumberManager = mock(ChangeNumberManager.class);
 | 
				
			||||||
 | 
					  private static RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager = mock(
 | 
				
			||||||
 | 
					      RegistrationRecoveryPasswordsManager.class);
 | 
				
			||||||
  private static ClientPresenceManager clientPresenceManager = mock(ClientPresenceManager.class);
 | 
					  private static ClientPresenceManager clientPresenceManager = mock(ClientPresenceManager.class);
 | 
				
			||||||
  private static TestClock testClock = TestClock.now();
 | 
					  private static TestClock testClock = TestClock.now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -210,6 +214,7 @@ class AccountControllerTest {
 | 
				
			||||||
          captchaChecker,
 | 
					          captchaChecker,
 | 
				
			||||||
          pushNotificationManager,
 | 
					          pushNotificationManager,
 | 
				
			||||||
          changeNumberManager,
 | 
					          changeNumberManager,
 | 
				
			||||||
 | 
					          registrationRecoveryPasswordsManager,
 | 
				
			||||||
          STORAGE_CREDENTIAL_GENERATOR,
 | 
					          STORAGE_CREDENTIAL_GENERATOR,
 | 
				
			||||||
          clientPresenceManager,
 | 
					          clientPresenceManager,
 | 
				
			||||||
          testClock))
 | 
					          testClock))
 | 
				
			||||||
| 
						 | 
					@ -1818,6 +1823,21 @@ class AccountControllerTest {
 | 
				
			||||||
    assertThat(response.getStatus()).isEqualTo(204);
 | 
					    assertThat(response.getStatus()).isEqualTo(204);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  void testAccountsAttributesUpdateRecoveryPassword() {
 | 
				
			||||||
 | 
					    final byte[] recoveryPassword = RandomUtils.nextBytes(32);
 | 
				
			||||||
 | 
					    final Response response =
 | 
				
			||||||
 | 
					        resources.getJerseyTest()
 | 
				
			||||||
 | 
					            .target("/v1/accounts/attributes/")
 | 
				
			||||||
 | 
					            .request()
 | 
				
			||||||
 | 
					            .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.UNDISCOVERABLE_UUID, AuthHelper.UNDISCOVERABLE_PASSWORD))
 | 
				
			||||||
 | 
					            .put(Entity.json(new AccountAttributes(false, 2222, null, null, true, null)
 | 
				
			||||||
 | 
					                .withRecoveryPassword(recoveryPassword)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assertThat(response.getStatus()).isEqualTo(204);
 | 
				
			||||||
 | 
					    verify(registrationRecoveryPasswordsManager).storeForCurrentNumber(eq(AuthHelper.UNDISCOVERABLE_NUMBER), eq(recoveryPassword));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testSetAccountAttributesDisableDiscovery() {
 | 
					  void testSetAccountAttributesDisableDiscovery() {
 | 
				
			||||||
    Response response =
 | 
					    Response response =
 | 
				
			||||||
| 
						 | 
					@ -1832,15 +1852,13 @@ class AccountControllerTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testSetAccountAttributesBadUnidentifiedKeyLength() {
 | 
					  void testSetAccountAttributesBadUnidentifiedKeyLength() {
 | 
				
			||||||
    final AccountAttributes attributes = new AccountAttributes(false, 2222, null, null, false, null);
 | 
					 | 
				
			||||||
    attributes.setUnidentifiedAccessKey(new byte[7]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Response response =
 | 
					    Response response =
 | 
				
			||||||
        resources.getJerseyTest()
 | 
					        resources.getJerseyTest()
 | 
				
			||||||
            .target("/v1/accounts/attributes/")
 | 
					            .target("/v1/accounts/attributes/")
 | 
				
			||||||
            .request()
 | 
					            .request()
 | 
				
			||||||
            .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
 | 
					            .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
 | 
				
			||||||
            .put(Entity.json(attributes));
 | 
					            .put(Entity.json(new AccountAttributes(false, 2222, null, null, false, null)
 | 
				
			||||||
 | 
					                .withUnidentifiedAccessKey(new byte[7])));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertThat(response.getStatus()).isEqualTo(422);
 | 
					    assertThat(response.getStatus()).isEqualTo(422);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 * Copyright 2013-2021 Signal Messenger, LLC
 | 
					 * Copyright 2013 Signal Messenger, LLC
 | 
				
			||||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -197,6 +197,7 @@ class AccountsManagerChangeNumberIntegrationTest {
 | 
				
			||||||
          secureBackupClient,
 | 
					          secureBackupClient,
 | 
				
			||||||
          clientPresenceManager,
 | 
					          clientPresenceManager,
 | 
				
			||||||
          mock(ExperimentEnrollmentManager.class),
 | 
					          mock(ExperimentEnrollmentManager.class),
 | 
				
			||||||
 | 
					          mock(RegistrationRecoveryPasswordsManager.class),
 | 
				
			||||||
          mock(Clock.class));
 | 
					          mock(Clock.class));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -164,6 +164,7 @@ class AccountsManagerConcurrentModificationIntegrationTest {
 | 
				
			||||||
          mock(SecureBackupClient.class),
 | 
					          mock(SecureBackupClient.class),
 | 
				
			||||||
          mock(ClientPresenceManager.class),
 | 
					          mock(ClientPresenceManager.class),
 | 
				
			||||||
          mock(ExperimentEnrollmentManager.class),
 | 
					          mock(ExperimentEnrollmentManager.class),
 | 
				
			||||||
 | 
					          mock(RegistrationRecoveryPasswordsManager.class),
 | 
				
			||||||
          mock(Clock.class)
 | 
					          mock(Clock.class)
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 * Copyright 2013-2022 Signal Messenger, LLC
 | 
					 * Copyright 2013 Signal Messenger, LLC
 | 
				
			||||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -154,6 +154,7 @@ class AccountsManagerTest {
 | 
				
			||||||
        backupClient,
 | 
					        backupClient,
 | 
				
			||||||
        mock(ClientPresenceManager.class),
 | 
					        mock(ClientPresenceManager.class),
 | 
				
			||||||
        enrollmentManager,
 | 
					        enrollmentManager,
 | 
				
			||||||
 | 
					        mock(RegistrationRecoveryPasswordsManager.class),
 | 
				
			||||||
        mock(Clock.class));
 | 
					        mock(Clock.class));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 * Copyright 2013-2021 Signal Messenger, LLC
 | 
					 * Copyright 2013 Signal Messenger, LLC
 | 
				
			||||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -185,6 +185,7 @@ class AccountsManagerUsernameIntegrationTest {
 | 
				
			||||||
        mock(SecureBackupClient.class),
 | 
					        mock(SecureBackupClient.class),
 | 
				
			||||||
        mock(ClientPresenceManager.class),
 | 
					        mock(ClientPresenceManager.class),
 | 
				
			||||||
        experimentEnrollmentManager,
 | 
					        experimentEnrollmentManager,
 | 
				
			||||||
 | 
					        mock(RegistrationRecoveryPasswordsManager.class),
 | 
				
			||||||
        mock(Clock.class));
 | 
					        mock(Clock.class));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,164 @@
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * Copyright 2023 Signal Messenger, LLC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					package org.whispersystems.textsecuregcm.storage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import static org.junit.jupiter.api.Assertions.assertEquals;
 | 
				
			||||||
 | 
					import static org.junit.jupiter.api.Assertions.assertFalse;
 | 
				
			||||||
 | 
					import static org.junit.jupiter.api.Assertions.assertTrue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.nio.charset.StandardCharsets;
 | 
				
			||||||
 | 
					import java.time.Clock;
 | 
				
			||||||
 | 
					import java.time.Duration;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					import java.util.Optional;
 | 
				
			||||||
 | 
					import java.util.concurrent.ExecutionException;
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
 | 
					import org.junit.jupiter.api.BeforeEach;
 | 
				
			||||||
 | 
					import org.junit.jupiter.api.Test;
 | 
				
			||||||
 | 
					import org.junit.jupiter.api.extension.RegisterExtension;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.util.AttributeValues;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.util.MockUtils;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.util.MutableClock;
 | 
				
			||||||
 | 
					import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
 | 
				
			||||||
 | 
					import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
 | 
				
			||||||
 | 
					import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
 | 
				
			||||||
 | 
					import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public class RegistrationRecoveryTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static final String TABLE_NAME = "registration_recovery_passwords";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static final MutableClock CLOCK = MockUtils.mutableClock(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static final Duration EXPIRATION = Duration.ofSeconds(1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static final String NUMBER = "+18005555555";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static final SaltedTokenHash ORIGINAL_HASH = SaltedTokenHash.generateFor("pass1");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static final SaltedTokenHash ANOTHER_HASH = SaltedTokenHash.generateFor("pass2");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @RegisterExtension
 | 
				
			||||||
 | 
					  private static final DynamoDbExtension DB_EXTENSION = DynamoDbExtension.builder()
 | 
				
			||||||
 | 
					      .tableName(TABLE_NAME)
 | 
				
			||||||
 | 
					      .hashKey(RegistrationRecoveryPasswords.KEY_E164)
 | 
				
			||||||
 | 
					      .attributeDefinition(AttributeDefinition.builder()
 | 
				
			||||||
 | 
					          .attributeName(RegistrationRecoveryPasswords.KEY_E164)
 | 
				
			||||||
 | 
					          .attributeType(ScalarAttributeType.S)
 | 
				
			||||||
 | 
					          .build())
 | 
				
			||||||
 | 
					      .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private RegistrationRecoveryPasswords store;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private RegistrationRecoveryPasswordsManager manager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @BeforeEach
 | 
				
			||||||
 | 
					  public void before() throws Exception {
 | 
				
			||||||
 | 
					    CLOCK.setTimeMillis(Clock.systemUTC().millis());
 | 
				
			||||||
 | 
					    store = new RegistrationRecoveryPasswords(
 | 
				
			||||||
 | 
					        DB_EXTENSION.getTableName(),
 | 
				
			||||||
 | 
					        EXPIRATION,
 | 
				
			||||||
 | 
					        DB_EXTENSION.getDynamoDbClient(),
 | 
				
			||||||
 | 
					        DB_EXTENSION.getDynamoDbAsyncClient(),
 | 
				
			||||||
 | 
					        CLOCK
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    manager = new RegistrationRecoveryPasswordsManager(store);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  public void testLookupAfterWrite() throws Exception {
 | 
				
			||||||
 | 
					    store.addOrReplace(NUMBER, ORIGINAL_HASH).get();
 | 
				
			||||||
 | 
					    final long initialExp = fetchTimestamp(NUMBER);
 | 
				
			||||||
 | 
					    final long expectedExpiration = CLOCK.instant().getEpochSecond() + EXPIRATION.getSeconds();
 | 
				
			||||||
 | 
					    assertEquals(expectedExpiration, initialExp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Optional<SaltedTokenHash> saltedTokenHash = store.lookup(NUMBER).get();
 | 
				
			||||||
 | 
					    assertTrue(saltedTokenHash.isPresent());
 | 
				
			||||||
 | 
					    assertEquals(ORIGINAL_HASH.salt(), saltedTokenHash.get().salt());
 | 
				
			||||||
 | 
					    assertEquals(ORIGINAL_HASH.hash(), saltedTokenHash.get().hash());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  public void testLookupAfterRefresh() throws Exception {
 | 
				
			||||||
 | 
					    store.addOrReplace(NUMBER, ORIGINAL_HASH).get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    CLOCK.increment(50, TimeUnit.SECONDS);
 | 
				
			||||||
 | 
					    store.addOrReplace(NUMBER, ORIGINAL_HASH).get();
 | 
				
			||||||
 | 
					    final long updatedExp = fetchTimestamp(NUMBER);
 | 
				
			||||||
 | 
					    final long expectedExp = CLOCK.instant().getEpochSecond() + EXPIRATION.getSeconds();
 | 
				
			||||||
 | 
					    assertEquals(expectedExp, updatedExp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Optional<SaltedTokenHash> saltedTokenHash = store.lookup(NUMBER).get();
 | 
				
			||||||
 | 
					    assertTrue(saltedTokenHash.isPresent());
 | 
				
			||||||
 | 
					    assertEquals(ORIGINAL_HASH.salt(), saltedTokenHash.get().salt());
 | 
				
			||||||
 | 
					    assertEquals(ORIGINAL_HASH.hash(), saltedTokenHash.get().hash());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  public void testReplace() throws Exception {
 | 
				
			||||||
 | 
					    store.addOrReplace(NUMBER, ORIGINAL_HASH).get();
 | 
				
			||||||
 | 
					    store.addOrReplace(NUMBER, ANOTHER_HASH).get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Optional<SaltedTokenHash> saltedTokenHash = store.lookup(NUMBER).get();
 | 
				
			||||||
 | 
					    assertTrue(saltedTokenHash.isPresent());
 | 
				
			||||||
 | 
					    assertEquals(ANOTHER_HASH.salt(), saltedTokenHash.get().salt());
 | 
				
			||||||
 | 
					    assertEquals(ANOTHER_HASH.hash(), saltedTokenHash.get().hash());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  public void testRemove() throws Exception {
 | 
				
			||||||
 | 
					    store.addOrReplace(NUMBER, ORIGINAL_HASH).get();
 | 
				
			||||||
 | 
					    assertTrue(store.lookup(NUMBER).get().isPresent());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    store.removeEntry(NUMBER).get();
 | 
				
			||||||
 | 
					    assertTrue(store.lookup(NUMBER).get().isEmpty());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  public void testManagerFlow() throws Exception {
 | 
				
			||||||
 | 
					    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.storeForCurrentNumber(NUMBER, password).get();
 | 
				
			||||||
 | 
					    assertTrue(manager.verify(NUMBER, password).get());
 | 
				
			||||||
 | 
					    assertFalse(manager.verify(NUMBER, wrongPassword).get());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update
 | 
				
			||||||
 | 
					    manager.storeForCurrentNumber(NUMBER, password).get();
 | 
				
			||||||
 | 
					    assertTrue(manager.verify(NUMBER, password).get());
 | 
				
			||||||
 | 
					    assertFalse(manager.verify(NUMBER, wrongPassword).get());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // replace
 | 
				
			||||||
 | 
					    manager.storeForCurrentNumber(NUMBER, updatedPassword).get();
 | 
				
			||||||
 | 
					    assertTrue(manager.verify(NUMBER, updatedPassword).get());
 | 
				
			||||||
 | 
					    assertFalse(manager.verify(NUMBER, password).get());
 | 
				
			||||||
 | 
					    assertFalse(manager.verify(NUMBER, wrongPassword).get());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    manager.removeForNumber(NUMBER).get();
 | 
				
			||||||
 | 
					    assertFalse(manager.verify(NUMBER, updatedPassword).get());
 | 
				
			||||||
 | 
					    assertFalse(manager.verify(NUMBER, password).get());
 | 
				
			||||||
 | 
					    assertFalse(manager.verify(NUMBER, wrongPassword).get());
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private static long fetchTimestamp(final String number) throws ExecutionException, InterruptedException {
 | 
				
			||||||
 | 
					    return DB_EXTENSION.getDynamoDbAsyncClient().getItem(GetItemRequest.builder()
 | 
				
			||||||
 | 
					            .tableName(DB_EXTENSION.getTableName())
 | 
				
			||||||
 | 
					            .key(Map.of(RegistrationRecoveryPasswords.KEY_E164, AttributeValues.fromString(number)))
 | 
				
			||||||
 | 
					            .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();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue