Retire non-atomic device-linking pathways
This commit is contained in:
		
							parent
							
								
									2054ab2771
								
							
						
					
					
						commit
						6c7a3df5ae
					
				| 
						 | 
					@ -802,8 +802,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
 | 
				
			||||||
            zkAuthOperations, callingGenericZkSecretParams, clock),
 | 
					            zkAuthOperations, callingGenericZkSecretParams, clock),
 | 
				
			||||||
        new ChallengeController(rateLimitChallengeManager, useRemoteAddress),
 | 
					        new ChallengeController(rateLimitChallengeManager, useRemoteAddress),
 | 
				
			||||||
        new DeviceController(config.getLinkDeviceSecretConfiguration().secret().value(), accountsManager,
 | 
					        new DeviceController(config.getLinkDeviceSecretConfiguration().secret().value(), accountsManager,
 | 
				
			||||||
            messagesManager, keysManager, rateLimiters,
 | 
					            rateLimiters, rateLimitersCluster, config.getMaxDevices(), clock),
 | 
				
			||||||
            rateLimitersCluster, config.getMaxDevices(), clock),
 | 
					 | 
				
			||||||
        new DirectoryV2Controller(directoryV2CredentialsGenerator),
 | 
					        new DirectoryV2Controller(directoryV2CredentialsGenerator),
 | 
				
			||||||
        new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
 | 
					        new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
 | 
				
			||||||
            ReceiptCredentialPresentation::new),
 | 
					            ReceiptCredentialPresentation::new),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,6 @@ import org.whispersystems.textsecuregcm.auth.AuthEnablementRefreshRequirementPro
 | 
				
			||||||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
 | 
					import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
 | 
					import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
 | 
					import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
 | 
					import org.whispersystems.textsecuregcm.entities.AccountAttributes;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest;
 | 
					import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
 | 
					import org.whispersystems.textsecuregcm.entities.DeviceInfo;
 | 
				
			||||||
| 
						 | 
					@ -65,9 +64,6 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Device;
 | 
					import org.whispersystems.textsecuregcm.storage.Device;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
 | 
					import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.DeviceSpec;
 | 
					import org.whispersystems.textsecuregcm.storage.DeviceSpec;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.Pair;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.Util;
 | 
					import org.whispersystems.textsecuregcm.util.Util;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.VerificationCode;
 | 
					import org.whispersystems.textsecuregcm.util.VerificationCode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -79,8 +75,6 @@ public class DeviceController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private final Key verificationTokenKey;
 | 
					  private final Key verificationTokenKey;
 | 
				
			||||||
  private final AccountsManager accounts;
 | 
					  private final AccountsManager accounts;
 | 
				
			||||||
  private final MessagesManager messages;
 | 
					 | 
				
			||||||
  private final KeysManager keys;
 | 
					 | 
				
			||||||
  private final RateLimiters rateLimiters;
 | 
					  private final RateLimiters rateLimiters;
 | 
				
			||||||
  private final FaultTolerantRedisCluster usedTokenCluster;
 | 
					  private final FaultTolerantRedisCluster usedTokenCluster;
 | 
				
			||||||
  private final Map<String, Integer> maxDeviceConfiguration;
 | 
					  private final Map<String, Integer> maxDeviceConfiguration;
 | 
				
			||||||
| 
						 | 
					@ -94,15 +88,11 @@ public class DeviceController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public DeviceController(byte[] linkDeviceSecret,
 | 
					  public DeviceController(byte[] linkDeviceSecret,
 | 
				
			||||||
      AccountsManager accounts,
 | 
					      AccountsManager accounts,
 | 
				
			||||||
      MessagesManager messages,
 | 
					 | 
				
			||||||
      KeysManager keys,
 | 
					 | 
				
			||||||
      RateLimiters rateLimiters,
 | 
					      RateLimiters rateLimiters,
 | 
				
			||||||
      FaultTolerantRedisCluster usedTokenCluster,
 | 
					      FaultTolerantRedisCluster usedTokenCluster,
 | 
				
			||||||
      Map<String, Integer> maxDeviceConfiguration, final Clock clock) {
 | 
					      Map<String, Integer> maxDeviceConfiguration, final Clock clock) {
 | 
				
			||||||
    this.verificationTokenKey = new SecretKeySpec(linkDeviceSecret, VERIFICATION_TOKEN_ALGORITHM);
 | 
					    this.verificationTokenKey = new SecretKeySpec(linkDeviceSecret, VERIFICATION_TOKEN_ALGORITHM);
 | 
				
			||||||
    this.accounts = accounts;
 | 
					    this.accounts = accounts;
 | 
				
			||||||
    this.messages = messages;
 | 
					 | 
				
			||||||
    this.keys = keys;
 | 
					 | 
				
			||||||
    this.rateLimiters = rateLimiters;
 | 
					    this.rateLimiters = rateLimiters;
 | 
				
			||||||
    this.usedTokenCluster = usedTokenCluster;
 | 
					    this.usedTokenCluster = usedTokenCluster;
 | 
				
			||||||
    this.maxDeviceConfiguration = maxDeviceConfiguration;
 | 
					    this.maxDeviceConfiguration = maxDeviceConfiguration;
 | 
				
			||||||
| 
						 | 
					@ -175,34 +165,6 @@ public class DeviceController {
 | 
				
			||||||
    return new VerificationCode(generateVerificationToken(account.getUuid()));
 | 
					    return new VerificationCode(generateVerificationToken(account.getUuid()));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * @deprecated callers should use {@link #linkDevice(BasicAuthorizationHeader, LinkDeviceRequest, ContainerRequest)}
 | 
					 | 
				
			||||||
   * instead
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  @PUT
 | 
					 | 
				
			||||||
  @Produces(MediaType.APPLICATION_JSON)
 | 
					 | 
				
			||||||
  @Consumes(MediaType.APPLICATION_JSON)
 | 
					 | 
				
			||||||
  @Path("/{verification_code}")
 | 
					 | 
				
			||||||
  @ChangesDeviceEnabledState
 | 
					 | 
				
			||||||
  @Deprecated(forRemoval = true)
 | 
					 | 
				
			||||||
  public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
 | 
					 | 
				
			||||||
      @HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader,
 | 
					 | 
				
			||||||
      @NotNull @Valid AccountAttributes accountAttributes,
 | 
					 | 
				
			||||||
      @Context ContainerRequest containerRequest)
 | 
					 | 
				
			||||||
      throws RateLimitExceededException, DeviceLimitExceededException {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final Pair<Account, Device> accountAndDevice = createDevice(authorizationHeader.getPassword(),
 | 
					 | 
				
			||||||
        verificationCode,
 | 
					 | 
				
			||||||
        accountAttributes,
 | 
					 | 
				
			||||||
        containerRequest,
 | 
					 | 
				
			||||||
        Optional.empty());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final Account account = accountAndDevice.first();
 | 
					 | 
				
			||||||
    final Device device = accountAndDevice.second();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return new DeviceResponse(account.getUuid(), account.getPhoneNumberIdentifier(), device.getId());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @PUT
 | 
					  @PUT
 | 
				
			||||||
  @Produces(MediaType.APPLICATION_JSON)
 | 
					  @Produces(MediaType.APPLICATION_JSON)
 | 
				
			||||||
  @Consumes(MediaType.APPLICATION_JSON)
 | 
					  @Consumes(MediaType.APPLICATION_JSON)
 | 
				
			||||||
| 
						 | 
					@ -219,21 +181,81 @@ public class DeviceController {
 | 
				
			||||||
  @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header(
 | 
					  @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header(
 | 
				
			||||||
      name = "Retry-After",
 | 
					      name = "Retry-After",
 | 
				
			||||||
      description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
 | 
					      description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
 | 
				
			||||||
  public DeviceResponse linkDevice(@HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader,
 | 
					  public CompletableFuture<DeviceResponse> linkDevice(@HeaderParam(HttpHeaders.AUTHORIZATION) BasicAuthorizationHeader authorizationHeader,
 | 
				
			||||||
      @NotNull @Valid LinkDeviceRequest linkDeviceRequest,
 | 
					      @NotNull @Valid LinkDeviceRequest linkDeviceRequest,
 | 
				
			||||||
      @Context ContainerRequest containerRequest)
 | 
					      @Context ContainerRequest containerRequest)
 | 
				
			||||||
      throws RateLimitExceededException, DeviceLimitExceededException {
 | 
					      throws RateLimitExceededException, DeviceLimitExceededException {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final Pair<Account, Device> accountAndDevice = createDevice(authorizationHeader.getPassword(),
 | 
					    final Optional<UUID> maybeAciFromToken = checkVerificationToken(linkDeviceRequest.verificationCode());
 | 
				
			||||||
        linkDeviceRequest.verificationCode(),
 | 
					 | 
				
			||||||
        linkDeviceRequest.accountAttributes(),
 | 
					 | 
				
			||||||
        containerRequest,
 | 
					 | 
				
			||||||
        Optional.of(linkDeviceRequest.deviceActivationRequest()));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final Account account = accountAndDevice.first();
 | 
					    final Account account = maybeAciFromToken.flatMap(accounts::getByAccountIdentifier)
 | 
				
			||||||
    final Device device = accountAndDevice.second();
 | 
					        .orElseThrow(ForbiddenException::new);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return new DeviceResponse(account.getUuid(), account.getPhoneNumberIdentifier(), device.getId());
 | 
					    final DeviceActivationRequest deviceActivationRequest = linkDeviceRequest.deviceActivationRequest();
 | 
				
			||||||
 | 
					    final AccountAttributes accountAttributes = linkDeviceRequest.accountAttributes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rateLimiters.getVerifyDeviceLimiter().validate(account.getUuid());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final boolean allKeysValid =
 | 
				
			||||||
 | 
					        PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.ACI),
 | 
				
			||||||
 | 
					            List.of(deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey()))
 | 
				
			||||||
 | 
					            && PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.PNI),
 | 
				
			||||||
 | 
					            List.of(deviceActivationRequest.pniSignedPreKey(), deviceActivationRequest.pniPqLastResortPreKey()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!allKeysValid) {
 | 
				
			||||||
 | 
					      throw new WebApplicationException(Response.status(422).build());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Normally, the "do we need to refresh somebody's websockets" listener can do this on its own. In this case,
 | 
				
			||||||
 | 
					    // we're not using the conventional authentication system, and so we need to give it a hint so it knows who the
 | 
				
			||||||
 | 
					    // active user is and what their device states look like.
 | 
				
			||||||
 | 
					    AuthEnablementRefreshRequirementProvider.setAccount(containerRequest, account);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    int maxDeviceLimit = MAX_DEVICES;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (maxDeviceConfiguration.containsKey(account.getNumber())) {
 | 
				
			||||||
 | 
					      maxDeviceLimit = maxDeviceConfiguration.get(account.getNumber());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (account.getDevices().size() >= maxDeviceLimit) {
 | 
				
			||||||
 | 
					      throw new DeviceLimitExceededException(account.getDevices().size(), maxDeviceLimit);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final DeviceCapabilities capabilities = accountAttributes.getCapabilities();
 | 
				
			||||||
 | 
					    if (capabilities != null && isCapabilityDowngrade(account, capabilities)) {
 | 
				
			||||||
 | 
					      throw new WebApplicationException(Response.status(409).build());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final String signalAgent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (deviceActivationRequest.apnToken().isPresent()) {
 | 
				
			||||||
 | 
					      signalAgent = "OWP";
 | 
				
			||||||
 | 
					    } else if (deviceActivationRequest.gcmToken().isPresent()) {
 | 
				
			||||||
 | 
					      signalAgent = "OWA";
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      signalAgent = "OWD";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return accounts.addDevice(account, new DeviceSpec(accountAttributes.getName(),
 | 
				
			||||||
 | 
					            authorizationHeader.getPassword(),
 | 
				
			||||||
 | 
					            signalAgent,
 | 
				
			||||||
 | 
					            capabilities,
 | 
				
			||||||
 | 
					            accountAttributes.getRegistrationId(),
 | 
				
			||||||
 | 
					            accountAttributes.getPhoneNumberIdentityRegistrationId(),
 | 
				
			||||||
 | 
					            accountAttributes.getFetchesMessages(),
 | 
				
			||||||
 | 
					            deviceActivationRequest.apnToken(),
 | 
				
			||||||
 | 
					            deviceActivationRequest.gcmToken(),
 | 
				
			||||||
 | 
					            deviceActivationRequest.aciSignedPreKey(),
 | 
				
			||||||
 | 
					            deviceActivationRequest.pniSignedPreKey(),
 | 
				
			||||||
 | 
					            deviceActivationRequest.aciPqLastResortPreKey(),
 | 
				
			||||||
 | 
					            deviceActivationRequest.pniPqLastResortPreKey()))
 | 
				
			||||||
 | 
					        .thenCompose(a -> usedTokenCluster.withCluster(connection -> connection.async()
 | 
				
			||||||
 | 
					                .set(getUsedTokenKey(linkDeviceRequest.verificationCode()), "", new SetArgs().ex(TOKEN_EXPIRATION_DURATION)))
 | 
				
			||||||
 | 
					            .thenApply(ignored -> a))
 | 
				
			||||||
 | 
					        .thenApply(accountAndDevice -> new DeviceResponse(
 | 
				
			||||||
 | 
					            accountAndDevice.first().getIdentifier(IdentityType.ACI),
 | 
				
			||||||
 | 
					            accountAndDevice.first().getIdentifier(IdentityType.PNI),
 | 
				
			||||||
 | 
					            accountAndDevice.second().getId()));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @PUT
 | 
					  @PUT
 | 
				
			||||||
| 
						 | 
					@ -338,111 +360,6 @@ public class DeviceController {
 | 
				
			||||||
    return account.isPniSupported() && !capabilities.pni();
 | 
					    return account.isPniSupported() && !capabilities.pni();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private Pair<Account, Device> createDevice(final String password,
 | 
					 | 
				
			||||||
      final String verificationCode,
 | 
					 | 
				
			||||||
      final AccountAttributes accountAttributes,
 | 
					 | 
				
			||||||
      final ContainerRequest containerRequest,
 | 
					 | 
				
			||||||
      final Optional<DeviceActivationRequest> maybeDeviceActivationRequest)
 | 
					 | 
				
			||||||
      throws RateLimitExceededException, DeviceLimitExceededException {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final Optional<UUID> maybeAciFromToken = checkVerificationToken(verificationCode);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final Account account = maybeAciFromToken.flatMap(accounts::getByAccountIdentifier)
 | 
					 | 
				
			||||||
        .orElseThrow(ForbiddenException::new);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    rateLimiters.getVerifyDeviceLimiter().validate(account.getUuid());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    maybeDeviceActivationRequest.ifPresent(deviceActivationRequest -> {
 | 
					 | 
				
			||||||
      final boolean allKeysValid =
 | 
					 | 
				
			||||||
          PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.ACI),
 | 
					 | 
				
			||||||
              List.of(deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey()))
 | 
					 | 
				
			||||||
              && PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.PNI),
 | 
					 | 
				
			||||||
              List.of(deviceActivationRequest.pniSignedPreKey(), deviceActivationRequest.pniPqLastResortPreKey()));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (!allKeysValid) {
 | 
					 | 
				
			||||||
        throw new WebApplicationException(Response.status(422).build());
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Normally, the "do we need to refresh somebody's websockets" listener can do this on its own. In this case,
 | 
					 | 
				
			||||||
    // we're not using the conventional authentication system, and so we need to give it a hint so it knows who the
 | 
					 | 
				
			||||||
    // active user is and what their device states look like.
 | 
					 | 
				
			||||||
    AuthEnablementRefreshRequirementProvider.setAccount(containerRequest, account);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    int maxDeviceLimit = MAX_DEVICES;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (maxDeviceConfiguration.containsKey(account.getNumber())) {
 | 
					 | 
				
			||||||
      maxDeviceLimit = maxDeviceConfiguration.get(account.getNumber());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (account.getDevices().size() >= maxDeviceLimit) {
 | 
					 | 
				
			||||||
      throw new DeviceLimitExceededException(account.getDevices().size(), maxDeviceLimit);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final DeviceCapabilities capabilities = accountAttributes.getCapabilities();
 | 
					 | 
				
			||||||
    if (capabilities != null && isCapabilityDowngrade(account, capabilities)) {
 | 
					 | 
				
			||||||
      throw new WebApplicationException(Response.status(409).build());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return maybeDeviceActivationRequest.map(deviceActivationRequest -> {
 | 
					 | 
				
			||||||
          final String signalAgent;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (deviceActivationRequest.apnToken().isPresent()) {
 | 
					 | 
				
			||||||
            signalAgent = "OWP";
 | 
					 | 
				
			||||||
          } else if (deviceActivationRequest.gcmToken().isPresent()) {
 | 
					 | 
				
			||||||
            signalAgent = "OWA";
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            signalAgent = "OWD";
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return accounts.addDevice(account, new DeviceSpec(accountAttributes.getName(),
 | 
					 | 
				
			||||||
                  password,
 | 
					 | 
				
			||||||
                  signalAgent,
 | 
					 | 
				
			||||||
                  capabilities,
 | 
					 | 
				
			||||||
                  accountAttributes.getRegistrationId(),
 | 
					 | 
				
			||||||
                  accountAttributes.getPhoneNumberIdentityRegistrationId(),
 | 
					 | 
				
			||||||
                  accountAttributes.getFetchesMessages(),
 | 
					 | 
				
			||||||
                  deviceActivationRequest.apnToken(),
 | 
					 | 
				
			||||||
                  deviceActivationRequest.gcmToken(),
 | 
					 | 
				
			||||||
                  deviceActivationRequest.aciSignedPreKey(),
 | 
					 | 
				
			||||||
                  deviceActivationRequest.pniSignedPreKey(),
 | 
					 | 
				
			||||||
                  deviceActivationRequest.aciPqLastResortPreKey(),
 | 
					 | 
				
			||||||
                  deviceActivationRequest.pniPqLastResortPreKey()))
 | 
					 | 
				
			||||||
              .thenCompose(a -> usedTokenCluster.withCluster(connection -> connection.async()
 | 
					 | 
				
			||||||
                      .set(getUsedTokenKey(verificationCode), "", new SetArgs().ex(TOKEN_EXPIRATION_DURATION)))
 | 
					 | 
				
			||||||
                  .thenApply(ignored -> a))
 | 
					 | 
				
			||||||
              .join();
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .orElseGet(() -> {
 | 
					 | 
				
			||||||
          final Device device = new Device();
 | 
					 | 
				
			||||||
          device.setName(accountAttributes.getName());
 | 
					 | 
				
			||||||
          device.setAuthTokenHash(SaltedTokenHash.generateFor(password));
 | 
					 | 
				
			||||||
          device.setFetchesMessages(accountAttributes.getFetchesMessages());
 | 
					 | 
				
			||||||
          device.setRegistrationId(accountAttributes.getRegistrationId());
 | 
					 | 
				
			||||||
          device.setPhoneNumberIdentityRegistrationId(accountAttributes.getPhoneNumberIdentityRegistrationId());
 | 
					 | 
				
			||||||
          device.setLastSeen(Util.todayInMillis());
 | 
					 | 
				
			||||||
          device.setCreated(System.currentTimeMillis());
 | 
					 | 
				
			||||||
          device.setCapabilities(accountAttributes.getCapabilities());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          final Account updatedAccount = accounts.update(account, a -> {
 | 
					 | 
				
			||||||
            device.setId(a.getNextDeviceId());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            CompletableFuture.allOf(
 | 
					 | 
				
			||||||
                    keys.delete(a.getUuid(), device.getId()),
 | 
					 | 
				
			||||||
                    keys.delete(a.getPhoneNumberIdentifier(), device.getId()),
 | 
					 | 
				
			||||||
                    messages.clear(a.getUuid(), device.getId()))
 | 
					 | 
				
			||||||
                .join();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            a.addDevice(device);
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          usedTokenCluster.useCluster(connection ->
 | 
					 | 
				
			||||||
              connection.sync().set(getUsedTokenKey(verificationCode), "", new SetArgs().ex(TOKEN_EXPIRATION_DURATION)));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          return new Pair<>(updatedAccount, device);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private static String getUsedTokenKey(final String token) {
 | 
					  private static String getUsedTokenKey(final String token) {
 | 
				
			||||||
    return "usedToken::" + token;
 | 
					    return "usedToken::" + token;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,6 @@ import static org.mockito.Mockito.mock;
 | 
				
			||||||
import static org.mockito.Mockito.never;
 | 
					import static org.mockito.Mockito.never;
 | 
				
			||||||
import static org.mockito.Mockito.reset;
 | 
					import static org.mockito.Mockito.reset;
 | 
				
			||||||
import static org.mockito.Mockito.verify;
 | 
					import static org.mockito.Mockito.verify;
 | 
				
			||||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
 | 
					 | 
				
			||||||
import static org.mockito.Mockito.when;
 | 
					import static org.mockito.Mockito.when;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import com.google.common.collect.ImmutableSet;
 | 
					import com.google.common.collect.ImmutableSet;
 | 
				
			||||||
| 
						 | 
					@ -49,7 +48,6 @@ import org.junit.jupiter.api.extension.RegisterExtension;
 | 
				
			||||||
import org.junit.jupiter.params.ParameterizedTest;
 | 
					import org.junit.jupiter.params.ParameterizedTest;
 | 
				
			||||||
import org.junit.jupiter.params.provider.Arguments;
 | 
					import org.junit.jupiter.params.provider.Arguments;
 | 
				
			||||||
import org.junit.jupiter.params.provider.MethodSource;
 | 
					import org.junit.jupiter.params.provider.MethodSource;
 | 
				
			||||||
import org.junit.jupiter.params.provider.ValueSource;
 | 
					 | 
				
			||||||
import org.mockito.ArgumentCaptor;
 | 
					import org.mockito.ArgumentCaptor;
 | 
				
			||||||
import org.signal.libsignal.protocol.IdentityKey;
 | 
					import org.signal.libsignal.protocol.IdentityKey;
 | 
				
			||||||
import org.signal.libsignal.protocol.ecc.Curve;
 | 
					import org.signal.libsignal.protocol.ecc.Curve;
 | 
				
			||||||
| 
						 | 
					@ -75,8 +73,6 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Device;
 | 
					import org.whispersystems.textsecuregcm.storage.Device;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
 | 
					import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.DeviceSpec;
 | 
					import org.whispersystems.textsecuregcm.storage.DeviceSpec;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.KeysManager;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.MessagesManager;
 | 
					 | 
				
			||||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
 | 
					import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
 | 
					import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
 | 
					import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
 | 
				
			||||||
| 
						 | 
					@ -91,8 +87,6 @@ import org.whispersystems.textsecuregcm.util.VerificationCode;
 | 
				
			||||||
class DeviceControllerTest {
 | 
					class DeviceControllerTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static AccountsManager accountsManager = mock(AccountsManager.class);
 | 
					  private static AccountsManager accountsManager = mock(AccountsManager.class);
 | 
				
			||||||
  private static MessagesManager messagesManager = mock(MessagesManager.class);
 | 
					 | 
				
			||||||
  private static KeysManager keysManager = mock(KeysManager.class);
 | 
					 | 
				
			||||||
  private static RateLimiters rateLimiters = mock(RateLimiters.class);
 | 
					  private static RateLimiters rateLimiters = mock(RateLimiters.class);
 | 
				
			||||||
  private static RateLimiter rateLimiter = mock(RateLimiter.class);
 | 
					  private static RateLimiter rateLimiter = mock(RateLimiter.class);
 | 
				
			||||||
  private static RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
 | 
					  private static RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
 | 
				
			||||||
| 
						 | 
					@ -109,8 +103,6 @@ class DeviceControllerTest {
 | 
				
			||||||
  private static DeviceController deviceController = new DeviceController(
 | 
					  private static DeviceController deviceController = new DeviceController(
 | 
				
			||||||
      generateLinkDeviceSecret(),
 | 
					      generateLinkDeviceSecret(),
 | 
				
			||||||
      accountsManager,
 | 
					      accountsManager,
 | 
				
			||||||
      messagesManager,
 | 
					 | 
				
			||||||
      keysManager,
 | 
					 | 
				
			||||||
      rateLimiters,
 | 
					      rateLimiters,
 | 
				
			||||||
      RedisClusterHelper.builder()
 | 
					      RedisClusterHelper.builder()
 | 
				
			||||||
          .stringCommands(commands)
 | 
					          .stringCommands(commands)
 | 
				
			||||||
| 
						 | 
					@ -160,19 +152,12 @@ class DeviceControllerTest {
 | 
				
			||||||
    when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount));
 | 
					    when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    AccountsHelper.setupMockUpdate(accountsManager);
 | 
					    AccountsHelper.setupMockUpdate(accountsManager);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
    when(keysManager.delete(any(), anyByte())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(messagesManager.clear(any(), anyByte())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @AfterEach
 | 
					  @AfterEach
 | 
				
			||||||
  void teardown() {
 | 
					  void teardown() {
 | 
				
			||||||
    reset(
 | 
					    reset(
 | 
				
			||||||
        accountsManager,
 | 
					        accountsManager,
 | 
				
			||||||
        messagesManager,
 | 
					 | 
				
			||||||
        keysManager,
 | 
					 | 
				
			||||||
        rateLimiters,
 | 
					        rateLimiters,
 | 
				
			||||||
        rateLimiter,
 | 
					        rateLimiter,
 | 
				
			||||||
        commands,
 | 
					        commands,
 | 
				
			||||||
| 
						 | 
					@ -186,95 +171,6 @@ class DeviceControllerTest {
 | 
				
			||||||
    testClock.unpin();
 | 
					    testClock.unpin();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					 | 
				
			||||||
  void validDeviceRegisterTest() {
 | 
					 | 
				
			||||||
    final Device existingDevice = mock(Device.class);
 | 
					 | 
				
			||||||
    when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID);
 | 
					 | 
				
			||||||
    when(existingDevice.isEnabled()).thenReturn(true);
 | 
					 | 
				
			||||||
    when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    VerificationCode deviceCode = resources.getJerseyTest()
 | 
					 | 
				
			||||||
        .target("/v1/devices/provisioning/code")
 | 
					 | 
				
			||||||
        .request()
 | 
					 | 
				
			||||||
        .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
 | 
					 | 
				
			||||||
        .get(VerificationCode.class);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    DeviceResponse response = resources.getJerseyTest()
 | 
					 | 
				
			||||||
        .target("/v1/devices/" + deviceCode.verificationCode())
 | 
					 | 
				
			||||||
        .request()
 | 
					 | 
				
			||||||
        .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
 | 
					 | 
				
			||||||
        .put(Entity.entity(new AccountAttributes(false, 1234, 5678,
 | 
					 | 
				
			||||||
                    null, null, true, null),
 | 
					 | 
				
			||||||
                MediaType.APPLICATION_JSON_TYPE),
 | 
					 | 
				
			||||||
            DeviceResponse.class);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertThat(response.getDeviceId()).isEqualTo(NEXT_DEVICE_ID);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    verify(messagesManager).clear(eq(AuthHelper.VALID_UUID), eq(NEXT_DEVICE_ID));
 | 
					 | 
				
			||||||
    verify(commands).set(anyString(), anyString(), any());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Test
 | 
					 | 
				
			||||||
  void validDeviceRegisterTestSignedTokenUsed() {
 | 
					 | 
				
			||||||
    when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final Device existingDevice = mock(Device.class);
 | 
					 | 
				
			||||||
    when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID);
 | 
					 | 
				
			||||||
    when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(commands.get(anyString())).thenReturn("");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final Response response = resources.getJerseyTest()
 | 
					 | 
				
			||||||
            .target("/v1/devices/" + verificationToken)
 | 
					 | 
				
			||||||
            .request()
 | 
					 | 
				
			||||||
            .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
 | 
					 | 
				
			||||||
            .put(Entity.entity(new AccountAttributes(false, 1234, 5678,
 | 
					 | 
				
			||||||
                    null, null, true, null),
 | 
					 | 
				
			||||||
                            MediaType.APPLICATION_JSON_TYPE));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Test
 | 
					 | 
				
			||||||
  void verifyDeviceWithNullAccountAttributes() {
 | 
					 | 
				
			||||||
    when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final Device existingDevice = mock(Device.class);
 | 
					 | 
				
			||||||
    when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID);
 | 
					 | 
				
			||||||
    when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    VerificationCode deviceCode = resources.getJerseyTest()
 | 
					 | 
				
			||||||
        .target("/v1/devices/provisioning/code")
 | 
					 | 
				
			||||||
        .request()
 | 
					 | 
				
			||||||
        .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
 | 
					 | 
				
			||||||
        .get(VerificationCode.class);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final Response response = resources.getJerseyTest()
 | 
					 | 
				
			||||||
        .target("/v1/devices/" + deviceCode.verificationCode())
 | 
					 | 
				
			||||||
        .request()
 | 
					 | 
				
			||||||
        .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
 | 
					 | 
				
			||||||
        .put(Entity.json(""));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertThat(response.getStatus()).isNotEqualTo(500);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Test
 | 
					 | 
				
			||||||
  void verifyDeviceTokenBadCredentials() {
 | 
					 | 
				
			||||||
    final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final Response response = resources.getJerseyTest()
 | 
					 | 
				
			||||||
        .target("/v1/devices/" + verificationToken)
 | 
					 | 
				
			||||||
        .request()
 | 
					 | 
				
			||||||
        .header("Authorization", "This is not a valid authorization header")
 | 
					 | 
				
			||||||
        .put(Entity.entity(new AccountAttributes(false, 1234, 5678,
 | 
					 | 
				
			||||||
                null, null, true, null),
 | 
					 | 
				
			||||||
            MediaType.APPLICATION_JSON_TYPE));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertEquals(401, response.getStatus());
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @ParameterizedTest
 | 
					  @ParameterizedTest
 | 
				
			||||||
  @MethodSource
 | 
					  @MethodSource
 | 
				
			||||||
  @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
 | 
					  @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
 | 
				
			||||||
| 
						 | 
					@ -318,9 +214,6 @@ class DeviceControllerTest {
 | 
				
			||||||
      return CompletableFuture.completedFuture(new Pair<>(a, deviceSpec.toDevice(NEXT_DEVICE_ID, testClock)));
 | 
					      return CompletableFuture.completedFuture(new Pair<>(a, deviceSpec.toDevice(NEXT_DEVICE_ID, testClock)));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
    when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
 | 
					    when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final AccountAttributes accountAttributes = new AccountAttributes(fetchesMessages, 1234, 5678, null, null, true, null);
 | 
					    final AccountAttributes accountAttributes = new AccountAttributes(fetchesMessages, 1234, 5678, null, null, true, null);
 | 
				
			||||||
| 
						 | 
					@ -371,6 +264,44 @@ class DeviceControllerTest {
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  void linkDeviceAtomicBadCredentials() {
 | 
				
			||||||
 | 
					    when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Device primaryDevice = mock(Device.class);
 | 
				
			||||||
 | 
					    when(primaryDevice.getId()).thenReturn(Device.PRIMARY_ID);
 | 
				
			||||||
 | 
					    when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(primaryDevice));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final ECSignedPreKey aciSignedPreKey;
 | 
				
			||||||
 | 
					    final ECSignedPreKey pniSignedPreKey;
 | 
				
			||||||
 | 
					    final KEMSignedPreKey aciPqLastResortPreKey;
 | 
				
			||||||
 | 
					    final KEMSignedPreKey pniPqLastResortPreKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
 | 
				
			||||||
 | 
					    final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair);
 | 
				
			||||||
 | 
					    pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair);
 | 
				
			||||||
 | 
					    aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair);
 | 
				
			||||||
 | 
					    pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
 | 
				
			||||||
 | 
					    when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID),
 | 
				
			||||||
 | 
					        new AccountAttributes(false, 1234, 5678, null, null, true, null),
 | 
				
			||||||
 | 
					        new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id"))));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try (final Response response = resources.getJerseyTest()
 | 
				
			||||||
 | 
					        .target("/v1/devices/link")
 | 
				
			||||||
 | 
					        .request()
 | 
				
			||||||
 | 
					        .header("Authorization", "This is not a valid authorization header")
 | 
				
			||||||
 | 
					        .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void linkDeviceAtomicWithVerificationTokenUsed() {
 | 
					  void linkDeviceAtomicWithVerificationTokenUsed() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -396,9 +327,6 @@ class DeviceControllerTest {
 | 
				
			||||||
    when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
 | 
					    when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
 | 
				
			||||||
    when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
 | 
					    when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
    when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(commands.get(anyString())).thenReturn("");
 | 
					    when(commands.get(anyString())).thenReturn("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID),
 | 
					    final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID),
 | 
				
			||||||
| 
						 | 
					@ -474,7 +402,6 @@ class DeviceControllerTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ParameterizedTest
 | 
					  @ParameterizedTest
 | 
				
			||||||
  @MethodSource
 | 
					  @MethodSource
 | 
				
			||||||
  @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
 | 
					 | 
				
			||||||
  void linkDeviceAtomicMissingProperty(final IdentityKey aciIdentityKey,
 | 
					  void linkDeviceAtomicMissingProperty(final IdentityKey aciIdentityKey,
 | 
				
			||||||
                                       final IdentityKey pniIdentityKey,
 | 
					                                       final IdentityKey pniIdentityKey,
 | 
				
			||||||
                                       final ECSignedPreKey aciSignedPreKey,
 | 
					                                       final ECSignedPreKey aciSignedPreKey,
 | 
				
			||||||
| 
						 | 
					@ -589,6 +516,45 @@ class DeviceControllerTest {
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Test
 | 
				
			||||||
 | 
					  void linkDeviceAtomicExcessiveDeviceName() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final Device existingDevice = mock(Device.class);
 | 
				
			||||||
 | 
					    when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID);
 | 
				
			||||||
 | 
					    when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final ECSignedPreKey aciSignedPreKey;
 | 
				
			||||||
 | 
					    final ECSignedPreKey pniSignedPreKey;
 | 
				
			||||||
 | 
					    final KEMSignedPreKey aciPqLastResortPreKey;
 | 
				
			||||||
 | 
					    final KEMSignedPreKey pniPqLastResortPreKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
 | 
				
			||||||
 | 
					    final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair);
 | 
				
			||||||
 | 
					    pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair);
 | 
				
			||||||
 | 
					    aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair);
 | 
				
			||||||
 | 
					    pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
 | 
				
			||||||
 | 
					    when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID),
 | 
				
			||||||
 | 
					        new AccountAttributes(false, 1234, 5678, TestRandomUtil.nextBytes(512), null, true, null),
 | 
				
			||||||
 | 
					        new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id"))));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try (final Response response = resources.getJerseyTest()
 | 
				
			||||||
 | 
					        .target("/v1/devices/link")
 | 
				
			||||||
 | 
					        .request()
 | 
				
			||||||
 | 
					        .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
 | 
				
			||||||
 | 
					        .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      assertEquals(422, response.getStatus());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ParameterizedTest
 | 
					  @ParameterizedTest
 | 
				
			||||||
  @MethodSource
 | 
					  @MethodSource
 | 
				
			||||||
  void linkDeviceRegistrationId(final int registrationId, final int pniRegistrationId, final int expectedStatusCode) {
 | 
					  void linkDeviceRegistrationId(final int registrationId, final int pniRegistrationId, final int expectedStatusCode) {
 | 
				
			||||||
| 
						 | 
					@ -620,9 +586,6 @@ class DeviceControllerTest {
 | 
				
			||||||
      return CompletableFuture.completedFuture(new Pair<>(a, deviceSpec.toDevice(NEXT_DEVICE_ID, testClock)));
 | 
					      return CompletableFuture.completedFuture(new Pair<>(a, deviceSpec.toDevice(NEXT_DEVICE_ID, testClock)));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
    when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
 | 
					    when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(),
 | 
					    final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(),
 | 
				
			||||||
| 
						 | 
					@ -675,41 +638,6 @@ class DeviceControllerTest {
 | 
				
			||||||
    assertThat(response.getStatus()).isEqualTo(401);
 | 
					    assertThat(response.getStatus()).isEqualTo(401);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					 | 
				
			||||||
  void invalidDeviceRegisterTest() {
 | 
					 | 
				
			||||||
    VerificationCode deviceCode = resources.getJerseyTest()
 | 
					 | 
				
			||||||
        .target("/v1/devices/provisioning/code")
 | 
					 | 
				
			||||||
        .request()
 | 
					 | 
				
			||||||
        .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
 | 
					 | 
				
			||||||
        .get(VerificationCode.class);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Response response = resources.getJerseyTest()
 | 
					 | 
				
			||||||
        .target("/v1/devices/" + deviceCode.verificationCode() + "-incorrect")
 | 
					 | 
				
			||||||
        .request()
 | 
					 | 
				
			||||||
        .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
 | 
					 | 
				
			||||||
        .put(Entity.entity(new AccountAttributes(false, 1234, 5678, null, null, true, null),
 | 
					 | 
				
			||||||
            MediaType.APPLICATION_JSON_TYPE));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertThat(response.getStatus()).isEqualTo(403);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    verifyNoMoreInteractions(messagesManager);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Test
 | 
					 | 
				
			||||||
  void oldDeviceRegisterTest() {
 | 
					 | 
				
			||||||
    Response response = resources.getJerseyTest()
 | 
					 | 
				
			||||||
        .target("/v1/devices/1112223")
 | 
					 | 
				
			||||||
        .request()
 | 
					 | 
				
			||||||
        .header("Authorization",
 | 
					 | 
				
			||||||
            AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO))
 | 
					 | 
				
			||||||
        .put(Entity.entity(new AccountAttributes(false, 1234, 5678, null, null, true, null),
 | 
					 | 
				
			||||||
            MediaType.APPLICATION_JSON_TYPE));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertThat(response.getStatus()).isEqualTo(403);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    verifyNoMoreInteractions(messagesManager);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void maxDevicesTest() {
 | 
					  void maxDevicesTest() {
 | 
				
			||||||
    final AuthHelper.TestAccount testAccount = AUTH_FILTER_EXTENSION.createTestAccount();
 | 
					    final AuthHelper.TestAccount testAccount = AUTH_FILTER_EXTENSION.createTestAccount();
 | 
				
			||||||
| 
						 | 
					@ -726,29 +654,15 @@ class DeviceControllerTest {
 | 
				
			||||||
        .get();
 | 
					        .get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertEquals(411, response.getStatus());
 | 
					    assertEquals(411, response.getStatus());
 | 
				
			||||||
    verifyNoMoreInteractions(messagesManager);
 | 
					    verify(accountsManager, never()).addDevice(any(), any());
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Test
 | 
					 | 
				
			||||||
  void longNameTest() {
 | 
					 | 
				
			||||||
    final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Response response = resources.getJerseyTest()
 | 
					 | 
				
			||||||
        .target("/v1/devices/" + verificationToken)
 | 
					 | 
				
			||||||
        .request()
 | 
					 | 
				
			||||||
        .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
 | 
					 | 
				
			||||||
        .put(Entity.entity(new AccountAttributes(false, 1234, 5678,
 | 
					 | 
				
			||||||
                TestRandomUtil.nextBytes(226), null, true, null),
 | 
					 | 
				
			||||||
            MediaType.APPLICATION_JSON_TYPE));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    assertEquals(422, response.getStatus());
 | 
					 | 
				
			||||||
    verifyNoMoreInteractions(messagesManager);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ParameterizedTest
 | 
					  @ParameterizedTest
 | 
				
			||||||
  @MethodSource
 | 
					  @MethodSource
 | 
				
			||||||
  void deviceDowngradePniTest(final boolean accountSupportsPni, final boolean deviceSupportsPni, final int expectedStatus) {
 | 
					  void deviceDowngradePniTest(final boolean accountSupportsPni, final boolean deviceSupportsPni, final int expectedStatus) {
 | 
				
			||||||
    when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
 | 
					    when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
 | 
				
			||||||
 | 
					    when(accountsManager.addDevice(any(), any()))
 | 
				
			||||||
 | 
					        .thenReturn(CompletableFuture.completedFuture(new Pair<>(mock(Account.class), mock(Device.class))));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final Device primaryDevice = mock(Device.class);
 | 
					    final Device primaryDevice = mock(Device.class);
 | 
				
			||||||
    when(primaryDevice.getId()).thenReturn(Device.PRIMARY_ID);
 | 
					    when(primaryDevice.getId()).thenReturn(Device.PRIMARY_ID);
 | 
				
			||||||
| 
						 | 
					@ -771,22 +685,10 @@ class DeviceControllerTest {
 | 
				
			||||||
    when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
 | 
					    when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
 | 
				
			||||||
    when(account.isPniSupported()).thenReturn(accountSupportsPni);
 | 
					    when(account.isPniSupported()).thenReturn(accountSupportsPni);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(accountsManager.addDevice(any(), any())).thenAnswer(invocation -> {
 | 
					 | 
				
			||||||
      final Account a = invocation.getArgument(0);
 | 
					 | 
				
			||||||
      final DeviceSpec deviceSpec = invocation.getArgument(1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return CompletableFuture.completedFuture(new Pair<>(a, deviceSpec.toDevice(NEXT_DEVICE_ID, testClock)));
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
    when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
 | 
					    when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final AccountAttributes accountAttributes = new AccountAttributes(false, 1234, 5678, null, null, true, new DeviceCapabilities(true, true, deviceSupportsPni, true));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID),
 | 
					    final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID),
 | 
				
			||||||
        accountAttributes,
 | 
					        new AccountAttributes(false, 1234, 5678, null, null, true, new DeviceCapabilities(true, true, deviceSupportsPni, true)),
 | 
				
			||||||
        new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id"))));
 | 
					        new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.of(new GcmRegistrationId("gcm-id"))));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try (final Response response = resources.getJerseyTest()
 | 
					    try (final Response response = resources.getJerseyTest()
 | 
				
			||||||
| 
						 | 
					@ -833,25 +735,6 @@ class DeviceControllerTest {
 | 
				
			||||||
    assertThat(response.getStatus()).isEqualTo(422);
 | 
					    assertThat(response.getStatus()).isEqualTo(422);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @ParameterizedTest
 | 
					 | 
				
			||||||
  @ValueSource(booleans = {true, false})
 | 
					 | 
				
			||||||
  void deviceDowngradePaymentActivationTest(boolean paymentActivation) {
 | 
					 | 
				
			||||||
    // Update when we start returning true value of capability & restricting downgrades
 | 
					 | 
				
			||||||
    DeviceCapabilities deviceCapabilities = new DeviceCapabilities(true, true, true, paymentActivation);
 | 
					 | 
				
			||||||
    AccountAttributes accountAttributes = new AccountAttributes(false, 1234, 5678, null, null, true, deviceCapabilities);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final String verificationToken = deviceController.generateVerificationToken(AuthHelper.VALID_UUID);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Response response = resources
 | 
					 | 
				
			||||||
        .getJerseyTest()
 | 
					 | 
				
			||||||
        .target("/v1/devices/" + verificationToken)
 | 
					 | 
				
			||||||
        .request()
 | 
					 | 
				
			||||||
        .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
 | 
					 | 
				
			||||||
        .header(HttpHeaders.USER_AGENT, "Signal-Android/5.42.8675309 Android/30")
 | 
					 | 
				
			||||||
        .put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
 | 
					 | 
				
			||||||
    assertThat(response.getStatus()).isEqualTo(200);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void removeDevice() {
 | 
					  void removeDevice() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue