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),
|
||||
new ChallengeController(rateLimitChallengeManager, useRemoteAddress),
|
||||
new DeviceController(config.getLinkDeviceSecretConfiguration().secret().value(), accountsManager,
|
||||
messagesManager, keysManager, rateLimiters,
|
||||
rateLimitersCluster, config.getMaxDevices(), clock),
|
||||
rateLimiters, rateLimitersCluster, config.getMaxDevices(), clock),
|
||||
new DirectoryV2Controller(directoryV2CredentialsGenerator),
|
||||
new DonationController(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(),
|
||||
ReceiptCredentialPresentation::new),
|
||||
|
|
|
@ -49,7 +49,6 @@ import org.whispersystems.textsecuregcm.auth.AuthEnablementRefreshRequirementPro
|
|||
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader;
|
||||
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
|
||||
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest;
|
||||
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.DeviceCapabilities;
|
||||
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.VerificationCode;
|
||||
|
||||
|
@ -79,8 +75,6 @@ public class DeviceController {
|
|||
|
||||
private final Key verificationTokenKey;
|
||||
private final AccountsManager accounts;
|
||||
private final MessagesManager messages;
|
||||
private final KeysManager keys;
|
||||
private final RateLimiters rateLimiters;
|
||||
private final FaultTolerantRedisCluster usedTokenCluster;
|
||||
private final Map<String, Integer> maxDeviceConfiguration;
|
||||
|
@ -94,15 +88,11 @@ public class DeviceController {
|
|||
|
||||
public DeviceController(byte[] linkDeviceSecret,
|
||||
AccountsManager accounts,
|
||||
MessagesManager messages,
|
||||
KeysManager keys,
|
||||
RateLimiters rateLimiters,
|
||||
FaultTolerantRedisCluster usedTokenCluster,
|
||||
Map<String, Integer> maxDeviceConfiguration, final Clock clock) {
|
||||
this.verificationTokenKey = new SecretKeySpec(linkDeviceSecret, VERIFICATION_TOKEN_ALGORITHM);
|
||||
this.accounts = accounts;
|
||||
this.messages = messages;
|
||||
this.keys = keys;
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.usedTokenCluster = usedTokenCluster;
|
||||
this.maxDeviceConfiguration = maxDeviceConfiguration;
|
||||
|
@ -175,34 +165,6 @@ public class DeviceController {
|
|||
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
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
|
@ -219,21 +181,81 @@ public class DeviceController {
|
|||
@ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header(
|
||||
name = "Retry-After",
|
||||
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,
|
||||
@Context ContainerRequest containerRequest)
|
||||
throws RateLimitExceededException, DeviceLimitExceededException {
|
||||
|
||||
final Pair<Account, Device> accountAndDevice = createDevice(authorizationHeader.getPassword(),
|
||||
linkDeviceRequest.verificationCode(),
|
||||
linkDeviceRequest.accountAttributes(),
|
||||
containerRequest,
|
||||
Optional.of(linkDeviceRequest.deviceActivationRequest()));
|
||||
final Optional<UUID> maybeAciFromToken = checkVerificationToken(linkDeviceRequest.verificationCode());
|
||||
|
||||
final Account account = accountAndDevice.first();
|
||||
final Device device = accountAndDevice.second();
|
||||
final Account account = maybeAciFromToken.flatMap(accounts::getByAccountIdentifier)
|
||||
.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
|
||||
|
@ -338,111 +360,6 @@ public class DeviceController {
|
|||
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) {
|
||||
return "usedToken::" + token;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import static org.mockito.Mockito.mock;
|
|||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
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.DeviceCapabilities;
|
||||
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.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.tests.util.KeysHelper;
|
||||
|
@ -91,8 +87,6 @@ import org.whispersystems.textsecuregcm.util.VerificationCode;
|
|||
class DeviceControllerTest {
|
||||
|
||||
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 RateLimiter rateLimiter = mock(RateLimiter.class);
|
||||
private static RedisAdvancedClusterCommands<String, String> commands = mock(RedisAdvancedClusterCommands.class);
|
||||
|
@ -109,8 +103,6 @@ class DeviceControllerTest {
|
|||
private static DeviceController deviceController = new DeviceController(
|
||||
generateLinkDeviceSecret(),
|
||||
accountsManager,
|
||||
messagesManager,
|
||||
keysManager,
|
||||
rateLimiters,
|
||||
RedisClusterHelper.builder()
|
||||
.stringCommands(commands)
|
||||
|
@ -160,19 +152,12 @@ class DeviceControllerTest {
|
|||
when(accountsManager.getByE164(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(maxedAccount));
|
||||
|
||||
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
|
||||
void teardown() {
|
||||
reset(
|
||||
accountsManager,
|
||||
messagesManager,
|
||||
keysManager,
|
||||
rateLimiters,
|
||||
rateLimiter,
|
||||
commands,
|
||||
|
@ -186,95 +171,6 @@ class DeviceControllerTest {
|
|||
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
|
||||
@MethodSource
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
|
@ -318,9 +214,6 @@ class DeviceControllerTest {
|
|||
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));
|
||||
|
||||
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
|
||||
void linkDeviceAtomicWithVerificationTokenUsed() {
|
||||
|
||||
|
@ -396,9 +327,6 @@ class DeviceControllerTest {
|
|||
when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.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("");
|
||||
|
||||
final LinkDeviceRequest request = new LinkDeviceRequest(deviceController.generateVerificationToken(AuthHelper.VALID_UUID),
|
||||
|
@ -474,7 +402,6 @@ class DeviceControllerTest {
|
|||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
void linkDeviceAtomicMissingProperty(final IdentityKey aciIdentityKey,
|
||||
final IdentityKey pniIdentityKey,
|
||||
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
|
||||
@MethodSource
|
||||
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)));
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(),
|
||||
|
@ -675,41 +638,6 @@ class DeviceControllerTest {
|
|||
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
|
||||
void maxDevicesTest() {
|
||||
final AuthHelper.TestAccount testAccount = AUTH_FILTER_EXTENSION.createTestAccount();
|
||||
|
@ -726,29 +654,15 @@ class DeviceControllerTest {
|
|||
.get();
|
||||
|
||||
assertEquals(411, response.getStatus());
|
||||
verifyNoMoreInteractions(messagesManager);
|
||||
}
|
||||
|
||||
@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);
|
||||
verify(accountsManager, never()).addDevice(any(), any());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void deviceDowngradePniTest(final boolean accountSupportsPni, final boolean deviceSupportsPni, final int expectedStatus) {
|
||||
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);
|
||||
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.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));
|
||||
|
||||
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),
|
||||
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"))));
|
||||
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
|
@ -833,25 +735,6 @@ class DeviceControllerTest {
|
|||
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
|
||||
void removeDevice() {
|
||||
|
||||
|
|
Loading…
Reference in New Issue