From 521900c048265da34ff4df36e2a3307bd257cb32 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Sat, 11 Nov 2023 10:07:16 -0800 Subject: [PATCH] Always require atomic account creation --- .../org/signal/integration/Operations.java | 43 +- .../org/signal/integration/AccountTest.java | 3 +- .../org/signal/integration/MessagingTest.java | 20 +- .../controllers/DeviceController.java | 54 +- .../controllers/RegistrationController.java | 62 +-- .../entities/AccountAttributes.java | 5 + .../entities/DeviceActivationRequest.java | 80 +-- .../entities/LinkDeviceRequest.java | 19 +- .../entities/RegistrationRequest.java | 99 ++-- .../util/OptionalIdentityKeyAdapter.java | 53 -- .../controllers/DeviceControllerTest.java | 89 ++-- .../RegistrationControllerTest.java | 466 ++++++++---------- 12 files changed, 409 insertions(+), 584 deletions(-) delete mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/util/OptionalIdentityKeyAdapter.java diff --git a/integration-tests/src/main/java/org/signal/integration/Operations.java b/integration-tests/src/main/java/org/signal/integration/Operations.java index acf60189c..243e08a88 100644 --- a/integration-tests/src/main/java/org/signal/integration/Operations.java +++ b/integration-tests/src/main/java/org/signal/integration/Operations.java @@ -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()); diff --git a/integration-tests/src/test/java/org/signal/integration/AccountTest.java b/integration-tests/src/test/java/org/signal/integration/AccountTest.java index 7b6e6c703..f028938c3 100644 --- a/integration-tests/src/test/java/org/signal/integration/AccountTest.java +++ b/integration-tests/src/test/java/org/signal/integration/AccountTest.java @@ -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 execute = Operations.apiGet("/v1/accounts/whoami") .authorized(user) diff --git a/integration-tests/src/test/java/org/signal/integration/MessagingTest.java b/integration-tests/src/test/java/org/signal/integration/MessagingTest.java index cf2af5e14..45ff46593 100644 --- a/integration-tests/src/test/java/org/signal/integration/MessagingTest.java +++ b/integration-tests/src/test/java/org/signal/integration/MessagingTest.java @@ -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); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java index 1dbd6400d..5cdab7484 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java @@ -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 maxDeviceConfiguration; + private final Map 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 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 createDevice(final String password, - final String verificationCode, - final AccountAttributes accountAttributes, - final ContainerRequest containerRequest, - final Optional maybeDeviceActivationRequest) + final String verificationCode, + final AccountAttributes accountAttributes, + final ContainerRequest containerRequest, + final Optional maybeDeviceActivationRequest) throws RateLimitExceededException, DeviceLimitExceededException { final Optional 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); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java index 4ab1a3bce..5d83df15a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java @@ -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(), diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java index 1e785582f..3771b24d4 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java @@ -122,4 +122,9 @@ public class AccountAttributes { this.recoveryPassword = recoveryPassword; return this; } + + @VisibleForTesting + public void setPhoneNumberIdentityRegistrationId(final Integer phoneNumberIdentityRegistrationId) { + this.phoneNumberIdentityRegistrationId = phoneNumberIdentityRegistrationId; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java index 57be1f6b7..eeed1d128 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java @@ -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) { } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceRequest.java index b12fb36a5..86e133b5b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceRequest.java @@ -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()) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationRequest.java index 68cc6b006..fb684f9b6 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationRequest.java @@ -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 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 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 aciIdentityKey, - @JsonProperty("pniIdentityKey") Optional 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 maybeIdentityKey, - final Optional> 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(); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/util/OptionalIdentityKeyAdapter.java b/service/src/main/java/org/whispersystems/textsecuregcm/util/OptionalIdentityKeyAdapter.java deleted file mode 100644 index 61a4b97cb..000000000 --- a/service/src/main/java/org/whispersystems/textsecuregcm/util/OptionalIdentityKeyAdapter.java +++ /dev/null @@ -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> { - - @Override - public void serialize(final Optional 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> { - - @Override - public Optional 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 getNullValue(DeserializationContext ctxt) { - return Optional.empty(); - } - } -} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java index 8be6a67c1..d5d60fa44 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java @@ -288,18 +288,18 @@ class DeviceControllerTest { .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .get(VerificationCode.class); - final Optional aciSignedPreKey; - final Optional pniSignedPreKey; - final Optional aciPqLastResortPreKey; - final Optional 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 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 aciSignedPreKey; - final Optional pniSignedPreKey; - final Optional aciPqLastResortPreKey; - final Optional 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 aciSignedPreKey; - final Optional pniSignedPreKey; - final Optional aciPqLastResortPreKey; - final Optional 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 aciSignedPreKey, - final Optional pniSignedPreKey, - final Optional aciPqLastResortPreKey, - final Optional 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 aciSignedPreKey = Optional.of(KeysHelper.signedECPreKey(1, aciIdentityKeyPair)); - final Optional pniSignedPreKey = Optional.of(KeysHelper.signedECPreKey(2, pniIdentityKeyPair)); - final Optional aciPqLastResortPreKey = Optional.of(KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair)); - final Optional 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") diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java index 97f3e25b7..adf25c345 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java @@ -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 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 registrationId, Optional 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 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 atomicAccountCreationConflictingChannel() { - final Optional aciIdentityKey; - final Optional pniIdentityKey; - final Optional aciSignedPreKey; - final Optional pniSignedPreKey; - final Optional aciPqLastResortPreKey; - final Optional 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 atomicAccountCreationPartialSignedPreKeys() { - final Optional aciIdentityKey; - final Optional pniIdentityKey; - final Optional aciSignedPreKey; - final Optional pniSignedPreKey; - final Optional aciPqLastResortPreKey; - final Optional 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 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 atomicAccountCreationSuccess() { - final Optional aciIdentityKey; - final Optional pniIdentityKey; - final Optional aciSignedPreKey; - final Optional pniSignedPreKey; - final Optional aciPqLastResortPreKey; - final Optional 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 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); - } }