Always require atomic account creation
This commit is contained in:
parent
9069c5abb6
commit
521900c048
|
@ -75,36 +75,6 @@ public final class Operations {
|
|||
|
||||
INTEGRATION_TOOLS.populateRecoveryPassword(number, registrationPassword).join();
|
||||
|
||||
// register account
|
||||
final RegistrationRequest registrationRequest = new RegistrationRequest(
|
||||
null, registrationPassword, accountAttributes, true, false,
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty());
|
||||
|
||||
final AccountIdentityResponse registrationResponse = apiPost("/v1/registration", registrationRequest)
|
||||
.authorized(number, accountPassword)
|
||||
.executeExpectSuccess(AccountIdentityResponse.class);
|
||||
|
||||
user.setAciUuid(registrationResponse.uuid());
|
||||
user.setPniUuid(registrationResponse.pni());
|
||||
|
||||
// upload pre-key
|
||||
final TestUser.PreKeySetPublicView preKeySetPublicView = user.preKeys(Device.PRIMARY_ID, false);
|
||||
apiPut("/v2/keys", preKeySetPublicView)
|
||||
.authorized(user, Device.PRIMARY_ID)
|
||||
.executeExpectSuccess();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public static TestUser newRegisteredUserAtomic(final String number) {
|
||||
final byte[] registrationPassword = RandomUtils.nextBytes(32);
|
||||
final String accountPassword = Base64.getEncoder().encodeToString(RandomUtils.nextBytes(32));
|
||||
|
||||
final TestUser user = TestUser.create(number, accountPassword, registrationPassword);
|
||||
final AccountAttributes accountAttributes = user.accountAttributes();
|
||||
|
||||
INTEGRATION_TOOLS.populateRecoveryPassword(number, registrationPassword).join();
|
||||
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
|
@ -113,13 +83,12 @@ public final class Operations {
|
|||
registrationPassword,
|
||||
accountAttributes,
|
||||
true,
|
||||
true,
|
||||
Optional.of(new IdentityKey(aciIdentityKeyPair.getPublicKey())),
|
||||
Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey())),
|
||||
Optional.of(generateSignedECPreKey(1, aciIdentityKeyPair)),
|
||||
Optional.of(generateSignedECPreKey(2, pniIdentityKeyPair)),
|
||||
Optional.of(generateSignedKEMPreKey(3, aciIdentityKeyPair)),
|
||||
Optional.of(generateSignedKEMPreKey(4, pniIdentityKeyPair)),
|
||||
new IdentityKey(aciIdentityKeyPair.getPublicKey()),
|
||||
new IdentityKey(pniIdentityKeyPair.getPublicKey()),
|
||||
generateSignedECPreKey(1, aciIdentityKeyPair),
|
||||
generateSignedECPreKey(2, pniIdentityKeyPair),
|
||||
generateSignedKEMPreKey(3, aciIdentityKeyPair),
|
||||
generateSignedKEMPreKey(4, pniIdentityKeyPair),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import org.signal.libsignal.usernames.Username;
|
|||
import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.EncryptedUsername;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
|
||||
|
@ -41,7 +40,7 @@ public class AccountTest {
|
|||
|
||||
@Test
|
||||
public void testCreateAccountAtomic() throws Exception {
|
||||
final TestUser user = Operations.newRegisteredUserAtomic("+19995550201");
|
||||
final TestUser user = Operations.newRegisteredUser("+19995550201");
|
||||
try {
|
||||
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
|
||||
.authorized(user)
|
||||
|
|
|
@ -11,8 +11,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
|
||||
|
@ -21,19 +20,10 @@ import org.whispersystems.textsecuregcm.storage.Device;
|
|||
|
||||
public class MessagingTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {true, false})
|
||||
public void testSendMessageUnsealed(final boolean atomicAccountCreation) throws Exception {
|
||||
final TestUser userA;
|
||||
final TestUser userB;
|
||||
|
||||
if (atomicAccountCreation) {
|
||||
userA = Operations.newRegisteredUserAtomic("+19995550102");
|
||||
userB = Operations.newRegisteredUserAtomic("+19995550103");
|
||||
} else {
|
||||
userA = Operations.newRegisteredUser("+19995550104");
|
||||
userB = Operations.newRegisteredUser("+19995550105");
|
||||
}
|
||||
@Test
|
||||
public void testSendMessageUnsealed() {
|
||||
final TestUser userA = Operations.newRegisteredUser("+19995550102");
|
||||
final TestUser userB = Operations.newRegisteredUser("+19995550103");
|
||||
|
||||
try {
|
||||
final byte[] expectedContent = "Hello, World!".getBytes(StandardCharsets.UTF_8);
|
||||
|
|
|
@ -77,12 +77,12 @@ public class DeviceController {
|
|||
static final int MAX_DEVICES = 6;
|
||||
|
||||
private final Key verificationTokenKey;
|
||||
private final AccountsManager accounts;
|
||||
private final MessagesManager messages;
|
||||
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 Map<String, Integer> maxDeviceConfiguration;
|
||||
private final Map<String, Integer> maxDeviceConfiguration;
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
|
@ -217,8 +217,8 @@ public class DeviceController {
|
|||
@ChangesDeviceEnabledState
|
||||
@Operation(summary = "Link a device to an account",
|
||||
description = """
|
||||
Links a device to an account identified by a given phone number.
|
||||
""")
|
||||
Links a device to an account identified by a given phone number.
|
||||
""")
|
||||
@ApiResponse(responseCode = "200", description = "The new device was linked to the calling account", useReturnTypeSchema = true)
|
||||
@ApiResponse(responseCode = "403", description = "The given account was not found or the given verification code was incorrect")
|
||||
@ApiResponse(responseCode = "411", description = "The given account already has its maximum number of linked devices")
|
||||
|
@ -227,8 +227,8 @@ public class DeviceController {
|
|||
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,
|
||||
@NotNull @Valid LinkDeviceRequest linkDeviceRequest,
|
||||
@Context ContainerRequest containerRequest)
|
||||
@NotNull @Valid LinkDeviceRequest linkDeviceRequest,
|
||||
@Context ContainerRequest containerRequest)
|
||||
throws RateLimitExceededException, DeviceLimitExceededException {
|
||||
|
||||
final Pair<Account, Device> accountAndDevice = createDevice(authorizationHeader.getPassword(),
|
||||
|
@ -296,7 +296,8 @@ public class DeviceController {
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
final byte[] expectedSignature = getInitializedMac().doFinal(claimsAndSignature[0].getBytes(StandardCharsets.UTF_8));
|
||||
final byte[] expectedSignature = getInitializedMac().doFinal(
|
||||
claimsAndSignature[0].getBytes(StandardCharsets.UTF_8));
|
||||
final byte[] providedSignature;
|
||||
|
||||
try {
|
||||
|
@ -345,10 +346,10 @@ public class DeviceController {
|
|||
}
|
||||
|
||||
private Pair<Account, Device> createDevice(final String password,
|
||||
final String verificationCode,
|
||||
final AccountAttributes accountAttributes,
|
||||
final ContainerRequest containerRequest,
|
||||
final Optional<DeviceActivationRequest> maybeDeviceActivationRequest)
|
||||
final String verificationCode,
|
||||
final AccountAttributes accountAttributes,
|
||||
final ContainerRequest containerRequest,
|
||||
final Optional<DeviceActivationRequest> maybeDeviceActivationRequest)
|
||||
throws RateLimitExceededException, DeviceLimitExceededException {
|
||||
|
||||
final Optional<UUID> maybeAciFromToken = checkVerificationToken(verificationCode);
|
||||
|
@ -359,16 +360,11 @@ public class DeviceController {
|
|||
rateLimiters.getVerifyDeviceLimiter().validate(account.getUuid());
|
||||
|
||||
maybeDeviceActivationRequest.ifPresent(deviceActivationRequest -> {
|
||||
assert deviceActivationRequest.aciSignedPreKey().isPresent();
|
||||
assert deviceActivationRequest.pniSignedPreKey().isPresent();
|
||||
assert deviceActivationRequest.aciPqLastResortPreKey().isPresent();
|
||||
assert deviceActivationRequest.pniPqLastResortPreKey().isPresent();
|
||||
|
||||
final boolean allKeysValid = PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(
|
||||
IdentityType.ACI),
|
||||
List.of(deviceActivationRequest.aciSignedPreKey().get(), deviceActivationRequest.aciPqLastResortPreKey().get()))
|
||||
&& PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.PNI),
|
||||
List.of(deviceActivationRequest.pniSignedPreKey().get(), deviceActivationRequest.pniPqLastResortPreKey().get()));
|
||||
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());
|
||||
|
@ -406,8 +402,8 @@ public class DeviceController {
|
|||
device.setCapabilities(accountAttributes.getCapabilities());
|
||||
|
||||
maybeDeviceActivationRequest.ifPresent(deviceActivationRequest -> {
|
||||
device.setSignedPreKey(deviceActivationRequest.aciSignedPreKey().get());
|
||||
device.setPhoneNumberIdentitySignedPreKey(deviceActivationRequest.pniSignedPreKey().get());
|
||||
device.setSignedPreKey(deviceActivationRequest.aciSignedPreKey());
|
||||
device.setPhoneNumberIdentitySignedPreKey(deviceActivationRequest.pniSignedPreKey());
|
||||
|
||||
deviceActivationRequest.apnToken().ifPresent(apnRegistrationId -> {
|
||||
device.setApnId(apnRegistrationId.apnRegistrationId());
|
||||
|
@ -431,13 +427,13 @@ public class DeviceController {
|
|||
|
||||
maybeDeviceActivationRequest.ifPresent(deviceActivationRequest -> CompletableFuture.allOf(
|
||||
keys.storeEcSignedPreKeys(a.getUuid(),
|
||||
Map.of(device.getId(), deviceActivationRequest.aciSignedPreKey().get())),
|
||||
Map.of(device.getId(), deviceActivationRequest.aciSignedPreKey())),
|
||||
keys.storePqLastResort(a.getUuid(),
|
||||
Map.of(device.getId(), deviceActivationRequest.aciPqLastResortPreKey().get())),
|
||||
Map.of(device.getId(), deviceActivationRequest.aciPqLastResortPreKey())),
|
||||
keys.storeEcSignedPreKeys(a.getPhoneNumberIdentifier(),
|
||||
Map.of(device.getId(), deviceActivationRequest.pniSignedPreKey().get())),
|
||||
Map.of(device.getId(), deviceActivationRequest.pniSignedPreKey())),
|
||||
keys.storePqLastResort(a.getPhoneNumberIdentifier(),
|
||||
Map.of(device.getId(), deviceActivationRequest.pniPqLastResortPreKey().get())))
|
||||
Map.of(device.getId(), deviceActivationRequest.pniPqLastResortPreKey())))
|
||||
.join());
|
||||
|
||||
a.addDevice(device);
|
||||
|
|
|
@ -64,7 +64,6 @@ public class RegistrationController {
|
|||
private static final String COUNTRY_CODE_TAG_NAME = "countryCode";
|
||||
private static final String REGION_CODE_TAG_NAME = "regionCode";
|
||||
private static final String VERIFICATION_TYPE_TAG_NAME = "verification";
|
||||
private static final String ACCOUNT_ACTIVATED_TAG_NAME = "accountActivated";
|
||||
private static final String INVALID_ACCOUNT_ATTRS_COUNTER_NAME = name(RegistrationController.class, "invalidAccountAttrs");
|
||||
|
||||
private final AccountsManager accounts;
|
||||
|
@ -145,50 +144,39 @@ public class RegistrationController {
|
|||
Account account = accounts.create(number, password, signalAgent, registrationRequest.accountAttributes(),
|
||||
existingAccount.map(Account::getBadges).orElseGet(ArrayList::new));
|
||||
|
||||
// If the request includes all the information we need to fully "activate" the account, we should do so
|
||||
if (registrationRequest.supportsAtomicAccountCreation()) {
|
||||
assert registrationRequest.aciIdentityKey().isPresent();
|
||||
assert registrationRequest.pniIdentityKey().isPresent();
|
||||
assert registrationRequest.deviceActivationRequest().aciSignedPreKey().isPresent();
|
||||
assert registrationRequest.deviceActivationRequest().pniSignedPreKey().isPresent();
|
||||
assert registrationRequest.deviceActivationRequest().aciPqLastResortPreKey().isPresent();
|
||||
assert registrationRequest.deviceActivationRequest().pniPqLastResortPreKey().isPresent();
|
||||
account = accounts.update(account, a -> {
|
||||
a.setIdentityKey(registrationRequest.aciIdentityKey());
|
||||
a.setPhoneNumberIdentityKey(registrationRequest.pniIdentityKey());
|
||||
|
||||
account = accounts.update(account, a -> {
|
||||
a.setIdentityKey(registrationRequest.aciIdentityKey().get());
|
||||
a.setPhoneNumberIdentityKey(registrationRequest.pniIdentityKey().get());
|
||||
final Device device = a.getPrimaryDevice().orElseThrow();
|
||||
|
||||
final Device device = a.getPrimaryDevice().orElseThrow();
|
||||
device.setSignedPreKey(registrationRequest.deviceActivationRequest().aciSignedPreKey());
|
||||
device.setPhoneNumberIdentitySignedPreKey(registrationRequest.deviceActivationRequest().pniSignedPreKey());
|
||||
|
||||
device.setSignedPreKey(registrationRequest.deviceActivationRequest().aciSignedPreKey().get());
|
||||
device.setPhoneNumberIdentitySignedPreKey(registrationRequest.deviceActivationRequest().pniSignedPreKey().get());
|
||||
|
||||
registrationRequest.deviceActivationRequest().apnToken().ifPresent(apnRegistrationId -> {
|
||||
device.setApnId(apnRegistrationId.apnRegistrationId());
|
||||
device.setVoipApnId(apnRegistrationId.voipRegistrationId());
|
||||
});
|
||||
|
||||
registrationRequest.deviceActivationRequest().gcmToken().ifPresent(gcmRegistrationId ->
|
||||
device.setGcmId(gcmRegistrationId.gcmRegistrationId()));
|
||||
|
||||
CompletableFuture.allOf(
|
||||
keysManager.storeEcSignedPreKeys(a.getUuid(),
|
||||
Map.of(Device.PRIMARY_ID, registrationRequest.deviceActivationRequest().aciSignedPreKey().get())),
|
||||
keysManager.storePqLastResort(a.getUuid(),
|
||||
Map.of(Device.PRIMARY_ID, registrationRequest.deviceActivationRequest().aciPqLastResortPreKey().get())),
|
||||
keysManager.storeEcSignedPreKeys(a.getPhoneNumberIdentifier(),
|
||||
Map.of(Device.PRIMARY_ID, registrationRequest.deviceActivationRequest().pniSignedPreKey().get())),
|
||||
keysManager.storePqLastResort(a.getPhoneNumberIdentifier(),
|
||||
Map.of(Device.PRIMARY_ID, registrationRequest.deviceActivationRequest().pniPqLastResortPreKey().get())))
|
||||
.join();
|
||||
registrationRequest.deviceActivationRequest().apnToken().ifPresent(apnRegistrationId -> {
|
||||
device.setApnId(apnRegistrationId.apnRegistrationId());
|
||||
device.setVoipApnId(apnRegistrationId.voipRegistrationId());
|
||||
});
|
||||
}
|
||||
|
||||
registrationRequest.deviceActivationRequest().gcmToken().ifPresent(gcmRegistrationId ->
|
||||
device.setGcmId(gcmRegistrationId.gcmRegistrationId()));
|
||||
|
||||
CompletableFuture.allOf(
|
||||
keysManager.storeEcSignedPreKeys(a.getUuid(),
|
||||
Map.of(Device.PRIMARY_ID, registrationRequest.deviceActivationRequest().aciSignedPreKey())),
|
||||
keysManager.storePqLastResort(a.getUuid(),
|
||||
Map.of(Device.PRIMARY_ID, registrationRequest.deviceActivationRequest().aciPqLastResortPreKey())),
|
||||
keysManager.storeEcSignedPreKeys(a.getPhoneNumberIdentifier(),
|
||||
Map.of(Device.PRIMARY_ID, registrationRequest.deviceActivationRequest().pniSignedPreKey())),
|
||||
keysManager.storePqLastResort(a.getPhoneNumberIdentifier(),
|
||||
Map.of(Device.PRIMARY_ID, registrationRequest.deviceActivationRequest().pniPqLastResortPreKey())))
|
||||
.join();
|
||||
});
|
||||
|
||||
Metrics.counter(ACCOUNT_CREATED_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)),
|
||||
Tag.of(REGION_CODE_TAG_NAME, Util.getRegion(number)),
|
||||
Tag.of(VERIFICATION_TYPE_TAG_NAME, verificationType.name()),
|
||||
Tag.of(ACCOUNT_ACTIVATED_TAG_NAME, String.valueOf(account.isEnabled()))))
|
||||
Tag.of(VERIFICATION_TYPE_TAG_NAME, verificationType.name())))
|
||||
.increment();
|
||||
|
||||
return new AccountIdentityResponse(account.getUuid(),
|
||||
|
|
|
@ -122,4 +122,9 @@ public class AccountAttributes {
|
|||
this.recoveryPassword = recoveryPassword;
|
||||
return this;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setPhoneNumberIdentityRegistrationId(final Integer phoneNumberIdentityRegistrationId) {
|
||||
this.phoneNumberIdentityRegistrationId = phoneNumberIdentityRegistrationId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,52 +3,52 @@ package org.whispersystems.textsecuregcm.entities;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record DeviceActivationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed EC pre-key to be associated with this account's ACI. If provided, an account
|
||||
will be created "atomically," and all other properties needed for atomic account
|
||||
creation must also be present.
|
||||
""")
|
||||
Optional<@Valid ECSignedPreKey> aciSignedPreKey,
|
||||
public record DeviceActivationRequest(
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
A signed EC pre-key to be associated with this account's ACI.
|
||||
""")
|
||||
ECSignedPreKey aciSignedPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed EC pre-key to be associated with this account's PNI. If provided, an account
|
||||
will be created "atomically," and all other properties needed for atomic account
|
||||
creation must also be present.
|
||||
""")
|
||||
Optional<@Valid ECSignedPreKey> pniSignedPreKey,
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
A signed EC pre-key to be associated with this account's PNI.
|
||||
""")
|
||||
ECSignedPreKey pniSignedPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed Kyber-1024 "last resort" pre-key to be associated with this account's ACI. If
|
||||
provided, an account will be created "atomically," and all other properties needed for
|
||||
atomic account creation must also be present.
|
||||
""")
|
||||
Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
A signed Kyber-1024 "last resort" pre-key to be associated with this account's ACI.
|
||||
""")
|
||||
KEMSignedPreKey aciPqLastResortPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed Kyber-1024 "last resort" pre-key to be associated with this account's PNI. If
|
||||
provided, an account will be created "atomically," and all other properties needed for
|
||||
atomic account creation must also be present.
|
||||
""")
|
||||
Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey,
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
A signed Kyber-1024 "last resort" pre-key to be associated with this account's PNI.
|
||||
""")
|
||||
KEMSignedPreKey pniPqLastResortPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
An APNs token set for the account's primary device. If provided, the account's primary
|
||||
device will be notified of new messages via push notifications to the given token. If
|
||||
creating an account "atomically," callers must provide exactly one of an APNs token
|
||||
set, an FCM token, or an `AccountAttributes` entity with `fetchesMessages` set to
|
||||
`true`.
|
||||
""")
|
||||
Optional<@Valid ApnRegistrationId> apnToken,
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
An APNs token set for the account's primary device. If provided, the account's primary
|
||||
device will be notified of new messages via push notifications to the given token.
|
||||
Callers must provide exactly one of an APNs token set, an FCM token, or an
|
||||
`AccountAttributes` entity with `fetchesMessages` set to `true`.
|
||||
""")
|
||||
Optional<@Valid ApnRegistrationId> apnToken,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
An FCM/GCM token for the account's primary device. If provided, the account's primary
|
||||
device will be notified of new messages via push notifications to the given token. If
|
||||
creating an account "atomically," callers must provide exactly one of an APNs token
|
||||
set, an FCM token, or an `AccountAttributes` entity with `fetchesMessages` set to
|
||||
`true`.
|
||||
""")
|
||||
Optional<@Valid GcmRegistrationId> gcmToken) {
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
An FCM/GCM token for the account's primary device. If provided, the account's primary
|
||||
device will be notified of new messages via push notifications to the given token.
|
||||
Callers must provide exactly one of an APNs token set, an FCM token, or an
|
||||
`AccountAttributes` entity with `fetchesMessages` set to `true`.
|
||||
""")
|
||||
Optional<@Valid GcmRegistrationId> gcmToken) {
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Optional;
|
||||
|
||||
public record LinkDeviceRequest(@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
|
@ -17,6 +18,8 @@ public record LinkDeviceRequest(@Schema(requiredMode = Schema.RequiredMode.REQUI
|
|||
|
||||
AccountAttributes accountAttributes,
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonUnwrapped
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
DeviceActivationRequest deviceActivationRequest) {
|
||||
|
@ -25,10 +28,10 @@ public record LinkDeviceRequest(@Schema(requiredMode = Schema.RequiredMode.REQUI
|
|||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public LinkDeviceRequest(@JsonProperty("verificationCode") String verificationCode,
|
||||
@JsonProperty("accountAttributes") AccountAttributes accountAttributes,
|
||||
@JsonProperty("aciSignedPreKey") Optional<@Valid ECSignedPreKey> aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") Optional<@Valid ECSignedPreKey> pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey,
|
||||
@JsonProperty("aciSignedPreKey") @NotNull @Valid ECSignedPreKey aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") @NotNull @Valid ECSignedPreKey pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") @NotNull @Valid KEMSignedPreKey aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") @NotNull @Valid KEMSignedPreKey pniPqLastResortPreKey,
|
||||
@JsonProperty("apnToken") Optional<@Valid ApnRegistrationId> apnToken,
|
||||
@JsonProperty("gcmToken") Optional<@Valid GcmRegistrationId> gcmToken) {
|
||||
|
||||
|
@ -36,14 +39,6 @@ public record LinkDeviceRequest(@Schema(requiredMode = Schema.RequiredMode.REQUI
|
|||
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnToken, gcmToken));
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
public boolean hasAllRequiredFields() {
|
||||
return deviceActivationRequest().aciSignedPreKey().isPresent()
|
||||
&& deviceActivationRequest().pniSignedPreKey().isPresent()
|
||||
&& deviceActivationRequest().aciPqLastResortPreKey().isPresent()
|
||||
&& deviceActivationRequest().pniPqLastResortPreKey().isPresent();
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
public boolean hasExactlyOneMessageDeliveryChannel() {
|
||||
if (accountAttributes.getFetchesMessages()) {
|
||||
|
|
|
@ -19,7 +19,7 @@ import javax.validation.constraints.AssertTrue;
|
|||
import javax.validation.constraints.NotNull;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.OptionalIdentityKeyAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
|
||||
|
||||
public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
The ID of an existing verification session as it appears in a verification session
|
||||
|
@ -50,31 +50,26 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
|
|||
""")
|
||||
boolean skipDeviceTransfer,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
If true, indicates that this is a request for "atomic" registration. If any properties
|
||||
needed for atomic account creation are not present, the request will fail. If false,
|
||||
atomic account creation can still occur, but only if all required fields are present.
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
The ACI-associated identity key for the account, encoded as a base64 string.
|
||||
""")
|
||||
boolean requireAtomic,
|
||||
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
|
||||
IdentityKey aciIdentityKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
The ACI-associated identity key for the account, encoded as a base64 string. If
|
||||
provided, an account will be created "atomically," and all other properties needed for
|
||||
atomic account creation must also be present.
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
The PNI-associated identity key for the account, encoded as a base64 string.
|
||||
""")
|
||||
@JsonSerialize(using = OptionalIdentityKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = OptionalIdentityKeyAdapter.Deserializer.class)
|
||||
Optional<IdentityKey> aciIdentityKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
The PNI-associated identity key for the account, encoded as a base64 string. If
|
||||
provided, an account will be created "atomically," and all other properties needed for
|
||||
atomic account creation must also be present.
|
||||
""")
|
||||
@JsonSerialize(using = OptionalIdentityKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = OptionalIdentityKeyAdapter.Deserializer.class)
|
||||
Optional<IdentityKey> pniIdentityKey,
|
||||
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
|
||||
IdentityKey pniIdentityKey,
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonUnwrapped
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
DeviceActivationRequest deviceActivationRequest) implements PhoneVerificationRequest {
|
||||
|
@ -85,65 +80,37 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
|
|||
@JsonProperty("recoveryPassword") byte[] recoveryPassword,
|
||||
@JsonProperty("accountAttributes") AccountAttributes accountAttributes,
|
||||
@JsonProperty("skipDeviceTransfer") boolean skipDeviceTransfer,
|
||||
@JsonProperty("requireAtomic") boolean requireAtomic,
|
||||
@JsonProperty("aciIdentityKey") Optional<IdentityKey> aciIdentityKey,
|
||||
@JsonProperty("pniIdentityKey") Optional<IdentityKey> pniIdentityKey,
|
||||
@JsonProperty("aciSignedPreKey") Optional<@Valid ECSignedPreKey> aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") Optional<@Valid ECSignedPreKey> pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey,
|
||||
@JsonProperty("aciIdentityKey") @NotNull @Valid IdentityKey aciIdentityKey,
|
||||
@JsonProperty("pniIdentityKey") @NotNull @Valid IdentityKey pniIdentityKey,
|
||||
@JsonProperty("aciSignedPreKey") @NotNull @Valid ECSignedPreKey aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") @NotNull @Valid ECSignedPreKey pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") @NotNull @Valid KEMSignedPreKey aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") @NotNull @Valid KEMSignedPreKey pniPqLastResortPreKey,
|
||||
@JsonProperty("apnToken") Optional<@Valid ApnRegistrationId> apnToken,
|
||||
@JsonProperty("gcmToken") Optional<@Valid GcmRegistrationId> gcmToken) {
|
||||
|
||||
// This may seem a little verbose, but at the time of writing, Jackson struggles with `@JsonUnwrapped` members in
|
||||
// records, and this is a workaround. Please see
|
||||
// https://github.com/FasterXML/jackson-databind/issues/3726#issuecomment-1525396869 for additional context.
|
||||
this(sessionId, recoveryPassword, accountAttributes, skipDeviceTransfer, requireAtomic, aciIdentityKey, pniIdentityKey,
|
||||
this(sessionId, recoveryPassword, accountAttributes, skipDeviceTransfer, aciIdentityKey, pniIdentityKey,
|
||||
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnToken, gcmToken));
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
public boolean isEverySignedKeyValid() {
|
||||
return validatePreKeySignature(aciIdentityKey(), deviceActivationRequest().aciSignedPreKey())
|
||||
&& validatePreKeySignature(pniIdentityKey(), deviceActivationRequest().pniSignedPreKey())
|
||||
&& validatePreKeySignature(aciIdentityKey(), deviceActivationRequest().aciPqLastResortPreKey())
|
||||
&& validatePreKeySignature(pniIdentityKey(), deviceActivationRequest().pniPqLastResortPreKey());
|
||||
}
|
||||
if (deviceActivationRequest().aciSignedPreKey() == null ||
|
||||
deviceActivationRequest().pniSignedPreKey() == null ||
|
||||
deviceActivationRequest().aciPqLastResortPreKey() == null ||
|
||||
deviceActivationRequest().pniPqLastResortPreKey() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private static boolean validatePreKeySignature(final Optional<IdentityKey> maybeIdentityKey,
|
||||
final Optional<? extends SignedPreKey<?>> maybeSignedPreKey) {
|
||||
|
||||
return maybeSignedPreKey.map(signedPreKey -> maybeIdentityKey
|
||||
.map(identityKey -> PreKeySignatureValidator.validatePreKeySignatures(identityKey, List.of(signedPreKey)))
|
||||
.orElse(false))
|
||||
.orElse(true);
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
public boolean isCompleteRequest() {
|
||||
final boolean hasNoAtomicAccountCreationParameters =
|
||||
aciIdentityKey().isEmpty()
|
||||
&& pniIdentityKey().isEmpty()
|
||||
&& deviceActivationRequest().aciSignedPreKey().isEmpty()
|
||||
&& deviceActivationRequest().pniSignedPreKey().isEmpty()
|
||||
&& deviceActivationRequest().aciPqLastResortPreKey().isEmpty()
|
||||
&& deviceActivationRequest().pniPqLastResortPreKey().isEmpty();
|
||||
|
||||
return supportsAtomicAccountCreation() || (!requireAtomic() && hasNoAtomicAccountCreationParameters);
|
||||
}
|
||||
|
||||
public boolean supportsAtomicAccountCreation() {
|
||||
return hasExactlyOneMessageDeliveryChannel()
|
||||
&& aciIdentityKey().isPresent()
|
||||
&& pniIdentityKey().isPresent()
|
||||
&& deviceActivationRequest().aciSignedPreKey().isPresent()
|
||||
&& deviceActivationRequest().pniSignedPreKey().isPresent()
|
||||
&& deviceActivationRequest().aciPqLastResortPreKey().isPresent()
|
||||
&& deviceActivationRequest().pniPqLastResortPreKey().isPresent();
|
||||
return PreKeySignatureValidator.validatePreKeySignatures(aciIdentityKey(), List.of(deviceActivationRequest().aciSignedPreKey(), deviceActivationRequest().aciPqLastResortPreKey()))
|
||||
&& PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey(), List.of(deviceActivationRequest().pniSignedPreKey(), deviceActivationRequest().pniPqLastResortPreKey()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@AssertTrue
|
||||
boolean hasExactlyOneMessageDeliveryChannel() {
|
||||
if (accountAttributes.getFetchesMessages()) {
|
||||
return deviceActivationRequest().apnToken().isEmpty() && deviceActivationRequest().gcmToken().isEmpty();
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
import java.util.Optional;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
|
||||
public class OptionalIdentityKeyAdapter {
|
||||
|
||||
public static class Serializer extends JsonSerializer<Optional<IdentityKey>> {
|
||||
|
||||
@Override
|
||||
public void serialize(final Optional<IdentityKey> maybePublicKey,
|
||||
final JsonGenerator jsonGenerator,
|
||||
final SerializerProvider serializers) throws IOException {
|
||||
|
||||
if (maybePublicKey.isPresent()) {
|
||||
jsonGenerator.writeString(Base64.getEncoder().encodeToString(maybePublicKey.get().serialize()));
|
||||
} else {
|
||||
jsonGenerator.writeNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonDeserializer<Optional<IdentityKey>> {
|
||||
|
||||
@Override
|
||||
public Optional<IdentityKey> deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
|
||||
try {
|
||||
return Optional.of(new IdentityKey(Base64.getDecoder().decode(jsonParser.getValueAsString())));
|
||||
} catch (final InvalidKeyException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<IdentityKey> getNullValue(DeserializationContext ctxt) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -288,18 +288,18 @@ class DeviceControllerTest {
|
|||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(VerificationCode.class);
|
||||
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey;
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey;
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey;
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey;
|
||||
final ECSignedPreKey aciSignedPreKey;
|
||||
final ECSignedPreKey pniSignedPreKey;
|
||||
final KEMSignedPreKey aciPqLastResortPreKey;
|
||||
final KEMSignedPreKey pniPqLastResortPreKey;
|
||||
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair));
|
||||
pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||
aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair));
|
||||
pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair));
|
||||
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()));
|
||||
|
@ -324,8 +324,8 @@ class DeviceControllerTest {
|
|||
|
||||
final Device device = deviceCaptor.getValue();
|
||||
|
||||
assertEquals(aciSignedPreKey.get(), device.getSignedPreKey(IdentityType.ACI));
|
||||
assertEquals(pniSignedPreKey.get(), device.getSignedPreKey(IdentityType.PNI));
|
||||
assertEquals(aciSignedPreKey, device.getSignedPreKey(IdentityType.ACI));
|
||||
assertEquals(pniSignedPreKey, device.getSignedPreKey(IdentityType.PNI));
|
||||
assertEquals(fetchesMessages, device.getFetchesMessages());
|
||||
|
||||
expectedApnsToken.ifPresentOrElse(expectedToken -> assertEquals(expectedToken, device.getApnId()),
|
||||
|
@ -338,14 +338,13 @@ class DeviceControllerTest {
|
|||
() -> assertNull(device.getGcmId()));
|
||||
|
||||
verify(messagesManager).clear(eq(AuthHelper.VALID_UUID), eq(NEXT_DEVICE_ID));
|
||||
verify(keysManager).storeEcSignedPreKeys(AuthHelper.VALID_UUID, Map.of(response.getDeviceId(), aciSignedPreKey.get()));
|
||||
verify(keysManager).storeEcSignedPreKeys(AuthHelper.VALID_PNI, Map.of(response.getDeviceId(), pniSignedPreKey.get()));
|
||||
verify(keysManager).storePqLastResort(AuthHelper.VALID_UUID, Map.of(response.getDeviceId(), aciPqLastResortPreKey.get()));
|
||||
verify(keysManager).storePqLastResort(AuthHelper.VALID_PNI, Map.of(response.getDeviceId(), pniPqLastResortPreKey.get()));
|
||||
verify(keysManager).storeEcSignedPreKeys(AuthHelper.VALID_UUID, Map.of(response.getDeviceId(), aciSignedPreKey));
|
||||
verify(keysManager).storeEcSignedPreKeys(AuthHelper.VALID_PNI, Map.of(response.getDeviceId(), pniSignedPreKey));
|
||||
verify(keysManager).storePqLastResort(AuthHelper.VALID_UUID, Map.of(response.getDeviceId(), aciPqLastResortPreKey));
|
||||
verify(keysManager).storePqLastResort(AuthHelper.VALID_PNI, Map.of(response.getDeviceId(), pniPqLastResortPreKey));
|
||||
verify(commands).set(anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static Stream<Arguments> linkDeviceAtomic() {
|
||||
final String apnsToken = "apns-token";
|
||||
final String apnsVoipToken = "apns-voip-token";
|
||||
|
@ -368,18 +367,18 @@ class DeviceControllerTest {
|
|||
when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID);
|
||||
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(existingDevice));
|
||||
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey;
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey;
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey;
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey;
|
||||
final ECSignedPreKey aciSignedPreKey;
|
||||
final ECSignedPreKey pniSignedPreKey;
|
||||
final KEMSignedPreKey aciPqLastResortPreKey;
|
||||
final KEMSignedPreKey pniPqLastResortPreKey;
|
||||
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair));
|
||||
pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||
aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair));
|
||||
pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair));
|
||||
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()));
|
||||
|
@ -421,18 +420,18 @@ class DeviceControllerTest {
|
|||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
|
||||
.get(VerificationCode.class);
|
||||
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey;
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey;
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey;
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey;
|
||||
final ECSignedPreKey aciSignedPreKey;
|
||||
final ECSignedPreKey pniSignedPreKey;
|
||||
final KEMSignedPreKey aciPqLastResortPreKey;
|
||||
final KEMSignedPreKey pniPqLastResortPreKey;
|
||||
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair));
|
||||
pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||
aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair));
|
||||
pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair));
|
||||
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()));
|
||||
|
@ -465,10 +464,10 @@ class DeviceControllerTest {
|
|||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
void linkDeviceAtomicMissingProperty(final IdentityKey aciIdentityKey,
|
||||
final IdentityKey pniIdentityKey,
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey,
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey,
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey) {
|
||||
final ECSignedPreKey aciSignedPreKey,
|
||||
final ECSignedPreKey pniSignedPreKey,
|
||||
final KEMSignedPreKey aciPqLastResortPreKey,
|
||||
final KEMSignedPreKey pniPqLastResortPreKey) {
|
||||
|
||||
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(AuthHelper.VALID_ACCOUNT));
|
||||
|
||||
|
@ -503,19 +502,19 @@ class DeviceControllerTest {
|
|||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair));
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair));
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair));
|
||||
final ECSignedPreKey aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair);
|
||||
final ECSignedPreKey pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair);
|
||||
final KEMSignedPreKey aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair);
|
||||
final KEMSignedPreKey pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair);
|
||||
|
||||
final IdentityKey aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey());
|
||||
final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
|
||||
|
||||
return Stream.of(
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, Optional.empty(), pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, Optional.empty(), aciPqLastResortPreKey, pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, Optional.empty(), pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, Optional.empty())
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, null, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, null, aciPqLastResortPreKey, pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, null, pniPqLastResortPreKey),
|
||||
Arguments.of(aciIdentityKey, pniIdentityKey, aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, null)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -545,7 +544,7 @@ class DeviceControllerTest {
|
|||
|
||||
final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.verificationCode(),
|
||||
new AccountAttributes(true, 1234, null, null, true, null),
|
||||
new DeviceActivationRequest(Optional.of(aciSignedPreKey), Optional.of(pniSignedPreKey), Optional.of(aciPqLastResortPreKey), Optional.of(pniPqLastResortPreKey), Optional.empty(), Optional.empty()));
|
||||
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, Optional.empty(), Optional.empty()));
|
||||
|
||||
try (final Response response = resources.getJerseyTest()
|
||||
.target("/v1/devices/link")
|
||||
|
|
|
@ -6,10 +6,8 @@
|
|||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.anyByte;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
@ -20,11 +18,11 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
|||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
@ -49,7 +47,6 @@ import org.junit.jupiter.params.ParameterizedTest;
|
|||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.junitpioneer.jupiter.cartesian.ArgumentSets;
|
||||
import org.junitpioneer.jupiter.cartesian.CartesianTest;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
|
@ -60,6 +57,7 @@ import org.whispersystems.textsecuregcm.auth.RegistrationLockError;
|
|||
import org.whispersystems.textsecuregcm.auth.RegistrationLockVerificationManager;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.ApnRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceActivationRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.GcmRegistrationId;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
|
@ -118,14 +116,20 @@ class RegistrationControllerTest {
|
|||
@BeforeEach
|
||||
void setUp() {
|
||||
when(rateLimiters.getRegistrationLimiter()).thenReturn(registrationLimiter);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistrationRequest() throws Exception {
|
||||
assertFalse(new RegistrationRequest("", new byte[0], new AccountAttributes(), true, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()).isValid());
|
||||
assertFalse(new RegistrationRequest("some", new byte[32], new AccountAttributes(), true, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()).isValid());
|
||||
assertTrue(new RegistrationRequest("", new byte[32], new AccountAttributes(), true, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()).isValid());
|
||||
assertTrue(new RegistrationRequest("some", new byte[0], new AccountAttributes(), true, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()).isValid());
|
||||
when(accountsManager.update(any(), any())).thenAnswer(invocation -> {
|
||||
final Account account = invocation.getArgument(0);
|
||||
final Consumer<Account> accountUpdater = invocation.getArgument(1);
|
||||
|
||||
accountUpdater.accept(account);
|
||||
|
||||
return invocation.getArgument(0);
|
||||
});
|
||||
|
||||
when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(keysManager.storeEcOneTimePreKeys(any(), anyByte(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
when(keysManager.storeKemOneTimePreKeys(any(), anyByte(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -151,32 +155,26 @@ class RegistrationControllerTest {
|
|||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource()
|
||||
@MethodSource
|
||||
void invalidRegistrationId(Optional<Integer> registrationId, Optional<Integer> pniRegistrationId, int statusCode) throws InterruptedException, JsonProcessingException {
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/registration")
|
||||
.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD));
|
||||
|
||||
when(registrationServiceClient.getSession(any(), any()))
|
||||
.thenReturn(
|
||||
CompletableFuture.completedFuture(
|
||||
Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null,
|
||||
SESSION_EXPIRATION_SECONDS))));
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getPrimaryDevice()).thenReturn(Optional.of(mock(Device.class)));
|
||||
|
||||
when(accountsManager.create(any(), any(), any(), any(), any()))
|
||||
.thenReturn(mock(Account.class));
|
||||
.thenReturn(account);
|
||||
|
||||
final String recoveryPassword = encodeRecoveryPassword(new byte[0]);
|
||||
|
||||
final Map<String, Object> accountAttrs = new HashMap<>();
|
||||
accountAttrs.put("recoveryPassword", recoveryPassword);
|
||||
registrationId.ifPresent(id -> accountAttrs.put("registrationId", id));
|
||||
pniRegistrationId.ifPresent(id -> accountAttrs.put("pniRegistrationId", id));
|
||||
final String json = SystemMapper.jsonMapper().writeValueAsString(Map.of(
|
||||
"sessionId", encodeSessionId("sessionId"),
|
||||
"recoveryPassword", recoveryPassword,
|
||||
"accountAttributes", accountAttrs,
|
||||
"skipDeviceTransfer", true
|
||||
));
|
||||
final String json = requestJson("sessionId", new byte[0], true, registrationId.orElse(0), pniRegistrationId);
|
||||
|
||||
try (Response response = request.post(Entity.json(json))) {
|
||||
assertEquals(statusCode, response.getStatus());
|
||||
|
@ -292,8 +290,12 @@ class RegistrationControllerTest {
|
|||
void recoveryPasswordManagerVerificationTrue() throws InterruptedException {
|
||||
when(registrationRecoveryPasswordsManager.verify(any(), any()))
|
||||
.thenReturn(CompletableFuture.completedFuture(true));
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getPrimaryDevice()).thenReturn(Optional.of(mock(Device.class)));
|
||||
|
||||
when(accountsManager.create(any(), any(), any(), any(), any()))
|
||||
.thenReturn(mock(Account.class));
|
||||
.thenReturn(account);
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/registration")
|
||||
|
@ -340,15 +342,19 @@ class RegistrationControllerTest {
|
|||
expectedStatus = 409;
|
||||
} else if (error != null) {
|
||||
final Exception e = switch (error) {
|
||||
case MISMATCH -> new WebApplicationException(error.getExpectedStatus());
|
||||
case RATE_LIMITED -> new RateLimitExceededException(null, true);
|
||||
};
|
||||
case MISMATCH -> new WebApplicationException(error.getExpectedStatus());
|
||||
case RATE_LIMITED -> new RateLimitExceededException(null, true);
|
||||
};
|
||||
doThrow(e)
|
||||
.when(registrationLockVerificationManager).verifyRegistrationLock(any(), any(), any(), any(), any());
|
||||
expectedStatus = error.getExpectedStatus();
|
||||
} else {
|
||||
final Account createdAccount = mock(Account.class);
|
||||
when(createdAccount.getPrimaryDevice()).thenReturn(Optional.of(mock(Device.class)));
|
||||
|
||||
when(accountsManager.create(any(), any(), any(), any(), any()))
|
||||
.thenReturn(mock(Account.class));
|
||||
.thenReturn(createdAccount);
|
||||
|
||||
expectedStatus = 200;
|
||||
}
|
||||
|
||||
|
@ -396,13 +402,17 @@ class RegistrationControllerTest {
|
|||
maybeAccount = Optional.empty();
|
||||
}
|
||||
when(accountsManager.getByE164(any())).thenReturn(maybeAccount);
|
||||
when(accountsManager.create(any(), any(), any(), any(), any())).thenReturn(mock(Account.class));
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getPrimaryDevice()).thenReturn(Optional.of(mock(Device.class)));
|
||||
|
||||
when(accountsManager.create(any(), any(), any(), any(), any())).thenReturn(account);
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/registration")
|
||||
.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD));
|
||||
try (Response response = request.post(Entity.json(requestJson("sessionId", new byte[0], skipDeviceTransfer)))) {
|
||||
try (Response response = request.post(Entity.json(requestJson("sessionId", new byte[0], skipDeviceTransfer, 1, Optional.of(2))))) {
|
||||
assertEquals(expectedStatus, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
@ -415,8 +425,12 @@ class RegistrationControllerTest {
|
|||
CompletableFuture.completedFuture(
|
||||
Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null,
|
||||
SESSION_EXPIRATION_SECONDS))));
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getPrimaryDevice()).thenReturn(Optional.of(mock(Device.class)));
|
||||
|
||||
when(accountsManager.create(any(), any(), any(), any(), any()))
|
||||
.thenReturn(mock(Account.class));
|
||||
.thenReturn(account);
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/registration")
|
||||
|
@ -447,22 +461,22 @@ class RegistrationControllerTest {
|
|||
}
|
||||
|
||||
static Stream<Arguments> atomicAccountCreationConflictingChannel() {
|
||||
final Optional<IdentityKey> aciIdentityKey;
|
||||
final Optional<IdentityKey> pniIdentityKey;
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey;
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey;
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey;
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey;
|
||||
final IdentityKey aciIdentityKey;
|
||||
final IdentityKey pniIdentityKey;
|
||||
final ECSignedPreKey aciSignedPreKey;
|
||||
final ECSignedPreKey pniSignedPreKey;
|
||||
final KEMSignedPreKey aciPqLastResortPreKey;
|
||||
final KEMSignedPreKey pniPqLastResortPreKey;
|
||||
{
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
aciIdentityKey = Optional.of(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
|
||||
pniIdentityKey = Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
|
||||
aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair));
|
||||
pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||
aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair));
|
||||
pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair));
|
||||
aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey());
|
||||
pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
|
||||
aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair);
|
||||
pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair);
|
||||
aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair);
|
||||
pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair);
|
||||
}
|
||||
|
||||
final AccountAttributes fetchesMessagesAccountAttributes =
|
||||
|
@ -477,7 +491,6 @@ class RegistrationControllerTest {
|
|||
new byte[0],
|
||||
fetchesMessagesAccountAttributes,
|
||||
true,
|
||||
false,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
|
@ -492,7 +505,6 @@ class RegistrationControllerTest {
|
|||
new byte[0],
|
||||
fetchesMessagesAccountAttributes,
|
||||
true,
|
||||
false,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
|
@ -507,7 +519,6 @@ class RegistrationControllerTest {
|
|||
new byte[0],
|
||||
pushAccountAttributes,
|
||||
true,
|
||||
false,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
|
@ -539,22 +550,22 @@ class RegistrationControllerTest {
|
|||
}
|
||||
|
||||
static Stream<Arguments> atomicAccountCreationPartialSignedPreKeys() {
|
||||
final Optional<IdentityKey> aciIdentityKey;
|
||||
final Optional<IdentityKey> pniIdentityKey;
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey;
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey;
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey;
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey;
|
||||
final IdentityKey aciIdentityKey;
|
||||
final IdentityKey pniIdentityKey;
|
||||
final ECSignedPreKey aciSignedPreKey;
|
||||
final ECSignedPreKey pniSignedPreKey;
|
||||
final KEMSignedPreKey aciPqLastResortPreKey;
|
||||
final KEMSignedPreKey pniPqLastResortPreKey;
|
||||
{
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
aciIdentityKey = Optional.of(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
|
||||
pniIdentityKey = Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
|
||||
aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair));
|
||||
pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||
aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair));
|
||||
pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair));
|
||||
aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey());
|
||||
pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
|
||||
aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair);
|
||||
pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair);
|
||||
aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair);
|
||||
pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair);
|
||||
}
|
||||
|
||||
final AccountAttributes accountAttributes =
|
||||
|
@ -566,11 +577,10 @@ class RegistrationControllerTest {
|
|||
new byte[0],
|
||||
accountAttributes,
|
||||
true,
|
||||
false,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
Optional.empty(),
|
||||
null,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.empty(),
|
||||
|
@ -581,10 +591,9 @@ class RegistrationControllerTest {
|
|||
new byte[0],
|
||||
accountAttributes,
|
||||
true,
|
||||
false,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
Optional.empty(),
|
||||
null,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
|
@ -596,13 +605,12 @@ class RegistrationControllerTest {
|
|||
new byte[0],
|
||||
accountAttributes,
|
||||
true,
|
||||
false,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
Optional.empty(),
|
||||
null,
|
||||
Optional.empty(),
|
||||
Optional.empty())),
|
||||
|
||||
|
@ -611,12 +619,11 @@ class RegistrationControllerTest {
|
|||
new byte[0],
|
||||
accountAttributes,
|
||||
true,
|
||||
false,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
Optional.empty(),
|
||||
null,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.empty(),
|
||||
Optional.empty())),
|
||||
|
@ -626,8 +633,7 @@ class RegistrationControllerTest {
|
|||
new byte[0],
|
||||
accountAttributes,
|
||||
true,
|
||||
false,
|
||||
Optional.empty(),
|
||||
null,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
|
@ -641,9 +647,8 @@ class RegistrationControllerTest {
|
|||
new byte[0],
|
||||
accountAttributes,
|
||||
true,
|
||||
false,
|
||||
aciIdentityKey,
|
||||
Optional.empty(),
|
||||
null,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
|
@ -689,13 +694,6 @@ class RegistrationControllerTest {
|
|||
|
||||
when(accountsManager.create(any(), any(), any(), any(), any())).thenReturn(account);
|
||||
|
||||
when(accountsManager.update(eq(account), any())).thenAnswer(invocation -> {
|
||||
final Consumer<Account> accountUpdater = invocation.getArgument(1);
|
||||
accountUpdater.accept(account);
|
||||
|
||||
return invocation.getArgument(0);
|
||||
});
|
||||
|
||||
when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
|
@ -730,60 +728,23 @@ class RegistrationControllerTest {
|
|||
() -> verify(device, never()).setGcmId(any()));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void nonAtomicAccountCreationWithNoAtomicFields(boolean requireAtomic) throws InterruptedException {
|
||||
when(registrationServiceClient.getSession(any(), any()))
|
||||
.thenReturn(
|
||||
CompletableFuture.completedFuture(
|
||||
Optional.of(new RegistrationServiceSession(new byte[16], NUMBER, true, null, null, null,
|
||||
SESSION_EXPIRATION_SECONDS))));
|
||||
|
||||
final Invocation.Builder request = resources.getJerseyTest()
|
||||
.target("/v1/registration")
|
||||
.request()
|
||||
.header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD));
|
||||
|
||||
when(accountsManager.create(any(), any(), any(), any(), any()))
|
||||
.thenReturn(mock(Account.class));
|
||||
|
||||
RegistrationRequest reg = new RegistrationRequest("session-id",
|
||||
new byte[0],
|
||||
new AccountAttributes(true, 1, "test", null, true, new Device.DeviceCapabilities(false, false, false, false)),
|
||||
true,
|
||||
requireAtomic,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty());
|
||||
|
||||
try (final Response response = request.post(Entity.json(reg))) {
|
||||
int expected = requireAtomic ? 422 : 200;
|
||||
assertEquals(expected, response.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream<Arguments> atomicAccountCreationSuccess() {
|
||||
final Optional<IdentityKey> aciIdentityKey;
|
||||
final Optional<IdentityKey> pniIdentityKey;
|
||||
final Optional<ECSignedPreKey> aciSignedPreKey;
|
||||
final Optional<ECSignedPreKey> pniSignedPreKey;
|
||||
final Optional<KEMSignedPreKey> aciPqLastResortPreKey;
|
||||
final Optional<KEMSignedPreKey> pniPqLastResortPreKey;
|
||||
final IdentityKey aciIdentityKey;
|
||||
final IdentityKey pniIdentityKey;
|
||||
final ECSignedPreKey aciSignedPreKey;
|
||||
final ECSignedPreKey pniSignedPreKey;
|
||||
final KEMSignedPreKey aciPqLastResortPreKey;
|
||||
final KEMSignedPreKey pniPqLastResortPreKey;
|
||||
{
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
aciIdentityKey = Optional.of(new IdentityKey(aciIdentityKeyPair.getPublicKey()));
|
||||
pniIdentityKey = Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()));
|
||||
aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair));
|
||||
pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair));
|
||||
aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair));
|
||||
pniPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair));
|
||||
aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey());
|
||||
pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
|
||||
aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair);
|
||||
pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair);
|
||||
aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair);
|
||||
pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair);
|
||||
}
|
||||
|
||||
final AccountAttributes fetchesMessagesAccountAttributes =
|
||||
|
@ -796,137 +757,154 @@ class RegistrationControllerTest {
|
|||
final String apnsVoipToken = "apns-voip-token";
|
||||
final String gcmToken = "gcm-token";
|
||||
|
||||
return Stream.of(false, true)
|
||||
// try with and without strict atomic checking
|
||||
.flatMap(requireAtomic ->
|
||||
Stream.of(
|
||||
// Fetches messages; no push tokens
|
||||
Arguments.of(new RegistrationRequest("session-id",
|
||||
new byte[0],
|
||||
fetchesMessagesAccountAttributes,
|
||||
true,
|
||||
requireAtomic,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.empty(),
|
||||
Optional.empty()),
|
||||
aciIdentityKey.get(),
|
||||
pniIdentityKey.get(),
|
||||
aciSignedPreKey.get(),
|
||||
pniSignedPreKey.get(),
|
||||
aciPqLastResortPreKey.get(),
|
||||
pniPqLastResortPreKey.get(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty()),
|
||||
return Stream.of(
|
||||
// Fetches messages; no push tokens
|
||||
Arguments.of(new RegistrationRequest("session-id",
|
||||
new byte[0],
|
||||
fetchesMessagesAccountAttributes,
|
||||
true,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.empty(),
|
||||
Optional.empty()),
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.empty()),
|
||||
|
||||
// Has APNs tokens
|
||||
Arguments.of(new RegistrationRequest("session-id",
|
||||
new byte[0],
|
||||
pushAccountAttributes,
|
||||
true,
|
||||
requireAtomic,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.of(new ApnRegistrationId(apnsToken, apnsVoipToken)),
|
||||
Optional.empty()),
|
||||
aciIdentityKey.get(),
|
||||
pniIdentityKey.get(),
|
||||
aciSignedPreKey.get(),
|
||||
pniSignedPreKey.get(),
|
||||
aciPqLastResortPreKey.get(),
|
||||
pniPqLastResortPreKey.get(),
|
||||
Optional.of(apnsToken),
|
||||
Optional.of(apnsVoipToken),
|
||||
Optional.empty()),
|
||||
// Has APNs tokens
|
||||
Arguments.of(new RegistrationRequest("session-id",
|
||||
new byte[0],
|
||||
pushAccountAttributes,
|
||||
true,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.of(new ApnRegistrationId(apnsToken, apnsVoipToken)),
|
||||
Optional.empty()),
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.of(apnsToken),
|
||||
Optional.of(apnsVoipToken),
|
||||
Optional.empty()),
|
||||
|
||||
// requires the request to be atomic
|
||||
Arguments.of(new RegistrationRequest("session-id",
|
||||
new byte[0],
|
||||
pushAccountAttributes,
|
||||
true,
|
||||
requireAtomic,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.of(new ApnRegistrationId(apnsToken, apnsVoipToken)),
|
||||
Optional.empty()),
|
||||
aciIdentityKey.get(),
|
||||
pniIdentityKey.get(),
|
||||
aciSignedPreKey.get(),
|
||||
pniSignedPreKey.get(),
|
||||
aciPqLastResortPreKey.get(),
|
||||
pniPqLastResortPreKey.get(),
|
||||
Optional.of(apnsToken),
|
||||
Optional.of(apnsVoipToken),
|
||||
Optional.empty()),
|
||||
// requires the request to be atomic
|
||||
Arguments.of(new RegistrationRequest("session-id",
|
||||
new byte[0],
|
||||
pushAccountAttributes,
|
||||
true,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.of(new ApnRegistrationId(apnsToken, apnsVoipToken)),
|
||||
Optional.empty()),
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.of(apnsToken),
|
||||
Optional.of(apnsVoipToken),
|
||||
Optional.empty()),
|
||||
|
||||
// Fetches messages; no push tokens
|
||||
Arguments.of(new RegistrationRequest("session-id",
|
||||
new byte[0],
|
||||
pushAccountAttributes,
|
||||
true,
|
||||
requireAtomic,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.empty(),
|
||||
Optional.of(new GcmRegistrationId(gcmToken))),
|
||||
aciIdentityKey.get(),
|
||||
pniIdentityKey.get(),
|
||||
aciSignedPreKey.get(),
|
||||
pniSignedPreKey.get(),
|
||||
aciPqLastResortPreKey.get(),
|
||||
pniPqLastResortPreKey.get(),
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of(gcmToken))));
|
||||
// Fetches messages; no push tokens
|
||||
Arguments.of(new RegistrationRequest("session-id",
|
||||
new byte[0],
|
||||
pushAccountAttributes,
|
||||
true,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.empty(),
|
||||
Optional.of(new GcmRegistrationId(gcmToken))),
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
aciSignedPreKey,
|
||||
pniSignedPreKey,
|
||||
aciPqLastResortPreKey,
|
||||
pniPqLastResortPreKey,
|
||||
Optional.empty(),
|
||||
Optional.empty(),
|
||||
Optional.of(gcmToken)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid request JSON with the give session ID and skipDeviceTransfer
|
||||
*/
|
||||
private static String requestJson(final String sessionId, final byte[] recoveryPassword, final boolean skipDeviceTransfer) {
|
||||
final String rp = encodeRecoveryPassword(recoveryPassword);
|
||||
return String.format("""
|
||||
{
|
||||
"sessionId": "%s",
|
||||
"recoveryPassword": "%s",
|
||||
"accountAttributes": {
|
||||
"recoveryPassword": "%s",
|
||||
"registrationId": 1
|
||||
},
|
||||
"skipDeviceTransfer": %s
|
||||
}
|
||||
""", encodeSessionId(sessionId), rp, rp, skipDeviceTransfer);
|
||||
private static String requestJson(final String sessionId,
|
||||
final byte[] recoveryPassword,
|
||||
final boolean skipDeviceTransfer,
|
||||
final int registrationId,
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<Integer> pniRegistrationId) {
|
||||
|
||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||
|
||||
final IdentityKey aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey());
|
||||
final IdentityKey pniIdentityKey = new IdentityKey(pniIdentityKeyPair.getPublicKey());
|
||||
|
||||
final AccountAttributes accountAttributes = new AccountAttributes(true, registrationId, "name", "reglock", true,
|
||||
new Device.DeviceCapabilities(true, true, true, true));
|
||||
|
||||
pniRegistrationId.ifPresent(accountAttributes::setPhoneNumberIdentityRegistrationId);
|
||||
|
||||
final RegistrationRequest request = new RegistrationRequest(
|
||||
Base64.getEncoder().encodeToString(sessionId.getBytes(StandardCharsets.UTF_8)),
|
||||
recoveryPassword,
|
||||
accountAttributes,
|
||||
skipDeviceTransfer,
|
||||
aciIdentityKey,
|
||||
pniIdentityKey,
|
||||
new DeviceActivationRequest(
|
||||
KeysHelper.signedECPreKey(1, aciIdentityKeyPair),
|
||||
KeysHelper.signedECPreKey(2, pniIdentityKeyPair),
|
||||
KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair),
|
||||
KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair),
|
||||
Optional.empty(),
|
||||
Optional.empty()));
|
||||
|
||||
try {
|
||||
return SystemMapper.jsonMapper().writerWithDefaultPrettyPrinter().writeValueAsString(request);
|
||||
} catch (final JsonProcessingException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid request JSON with the given session ID
|
||||
*/
|
||||
private static String requestJson(final String sessionId) {
|
||||
return requestJson(sessionId, new byte[0], false);
|
||||
return requestJson(sessionId, new byte[0], false, 1, Optional.of(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid request JSON with the given Recovery Password
|
||||
*/
|
||||
private static String requestJsonRecoveryPassword(final byte[] recoveryPassword) {
|
||||
return requestJson("", recoveryPassword, false);
|
||||
return requestJson("", recoveryPassword, false, 1, Optional.of(2));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -953,12 +931,4 @@ class RegistrationControllerTest {
|
|||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private static String encodeSessionId(final String sessionId) {
|
||||
return Base64.getUrlEncoder().encodeToString(sessionId.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private static String encodeRecoveryPassword(final byte[] recoveryPassword) {
|
||||
return Base64.getEncoder().encodeToString(recoveryPassword);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue