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 33e26b92d..6565e8996 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java @@ -21,7 +21,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; -import java.util.Collections; import java.util.Map; import java.util.Optional; import javax.validation.Valid; @@ -95,7 +94,7 @@ public class RegistrationController { @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Registers an account", description = """ - Registers a new account or attempts to “re-register” an existing account. It is expected that a well-behaved client + Registers a new account or attempts to “re-register” an existing account. It is expected that a well-behaved client could make up to three consecutive calls to this API: 1. gets 423 from existing registration lock \n 2. gets 409 from device available for transfer \n @@ -150,10 +149,10 @@ public class RegistrationController { if (registrationRequest.supportsAtomicAccountCreation()) { assert registrationRequest.aciIdentityKey().isPresent(); assert registrationRequest.pniIdentityKey().isPresent(); - assert registrationRequest.aciSignedPreKey().isPresent(); - assert registrationRequest.pniSignedPreKey().isPresent(); - assert registrationRequest.aciPqLastResortPreKey().isPresent(); - assert registrationRequest.pniPqLastResortPreKey().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().get()); @@ -161,19 +160,19 @@ public class RegistrationController { final Device device = a.getMasterDevice().orElseThrow(); - device.setSignedPreKey(registrationRequest.aciSignedPreKey().get()); - device.setPhoneNumberIdentitySignedPreKey(registrationRequest.pniSignedPreKey().get()); + device.setSignedPreKey(registrationRequest.deviceActivationRequest().aciSignedPreKey().get()); + device.setPhoneNumberIdentitySignedPreKey(registrationRequest.deviceActivationRequest().pniSignedPreKey().get()); - registrationRequest.apnToken().ifPresent(apnRegistrationId -> { + registrationRequest.deviceActivationRequest().apnToken().ifPresent(apnRegistrationId -> { device.setApnId(apnRegistrationId.apnRegistrationId()); device.setVoipApnId(apnRegistrationId.voipRegistrationId()); }); - registrationRequest.gcmToken().ifPresent(gcmRegistrationId -> + registrationRequest.deviceActivationRequest().gcmToken().ifPresent(gcmRegistrationId -> device.setGcmId(gcmRegistrationId.gcmRegistrationId())); - keys.storePqLastResort(a.getUuid(), Map.of(Device.MASTER_ID, registrationRequest.aciPqLastResortPreKey().get())); - keys.storePqLastResort(a.getPhoneNumberIdentifier(), Map.of(Device.MASTER_ID, registrationRequest.pniPqLastResortPreKey().get())); + keys.storePqLastResort(a.getUuid(), Map.of(Device.MASTER_ID, registrationRequest.deviceActivationRequest().aciPqLastResortPreKey().get())); + keys.storePqLastResort(a.getPhoneNumberIdentifier(), Map.of(Device.MASTER_ID, registrationRequest.deviceActivationRequest().pniPqLastResortPreKey().get())); }); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java new file mode 100644 index 000000000..83add0f9f --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java @@ -0,0 +1,53 @@ +package org.whispersystems.textsecuregcm.entities; + +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.validation.Valid; +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 SignedPreKey> 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 SignedPreKey> 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 SignedPreKey> 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 SignedPreKey> 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 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) { +} 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 20254a127..3dddb6269 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationRequest.java @@ -5,6 +5,9 @@ package org.whispersystems.textsecuregcm.entities; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.common.annotations.VisibleForTesting; import io.swagger.v3.oas.annotations.media.Schema; @@ -59,58 +62,38 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT """) Optional pniIdentityKey, - @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 SignedPreKey> aciSignedPreKey, + @JsonUnwrapped + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + DeviceActivationRequest deviceActivationRequest) implements PhoneVerificationRequest { - @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 SignedPreKey> pniSignedPreKey, + @JsonCreator + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public RegistrationRequest(@JsonProperty("sessionId") String sessionId, + @JsonProperty("recoveryPassword") byte[] recoveryPassword, + @JsonProperty("accountAttributes") AccountAttributes accountAttributes, + @JsonProperty("skipDeviceTransfer") boolean skipDeviceTransfer, + @JsonProperty("aciIdentityKey") Optional aciIdentityKey, + @JsonProperty("pniIdentityKey") Optional pniIdentityKey, + @JsonProperty("aciSignedPreKey") Optional<@Valid SignedPreKey> aciSignedPreKey, + @JsonProperty("pniSignedPreKey") Optional<@Valid SignedPreKey> pniSignedPreKey, + @JsonProperty("aciPqLastResortPreKey") Optional<@Valid SignedPreKey> aciPqLastResortPreKey, + @JsonProperty("pniPqLastResortPreKey") Optional<@Valid SignedPreKey> pniPqLastResortPreKey, + @JsonProperty("apnToken") Optional<@Valid ApnRegistrationId> apnToken, + @JsonProperty("gcmToken") Optional<@Valid GcmRegistrationId> gcmToken) { - @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 SignedPreKey> 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 SignedPreKey> 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 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) implements PhoneVerificationRequest { + // 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, aciIdentityKey, pniIdentityKey, + new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnToken, gcmToken)); + } @AssertTrue public boolean isEverySignedKeyValid() { - return validatePreKeySignature(aciIdentityKey(), aciSignedPreKey()) - && validatePreKeySignature(pniIdentityKey(), pniSignedPreKey()) - && validatePreKeySignature(aciIdentityKey(), aciPqLastResortPreKey()) - && validatePreKeySignature(pniIdentityKey(), pniPqLastResortPreKey()); + return validatePreKeySignature(aciIdentityKey(), deviceActivationRequest().aciSignedPreKey()) + && validatePreKeySignature(pniIdentityKey(), deviceActivationRequest().pniSignedPreKey()) + && validatePreKeySignature(aciIdentityKey(), deviceActivationRequest().aciPqLastResortPreKey()) + && validatePreKeySignature(pniIdentityKey(), deviceActivationRequest().pniPqLastResortPreKey()); } @SuppressWarnings("OptionalUsedAsFieldOrParameterType") @@ -128,10 +111,10 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT final boolean hasNoAtomicAccountCreationParameters = aciIdentityKey().isEmpty() && pniIdentityKey().isEmpty() - && aciSignedPreKey().isEmpty() - && pniSignedPreKey().isEmpty() - && aciPqLastResortPreKey().isEmpty() - && pniPqLastResortPreKey().isEmpty(); + && deviceActivationRequest().aciSignedPreKey().isEmpty() + && deviceActivationRequest().pniSignedPreKey().isEmpty() + && deviceActivationRequest().aciPqLastResortPreKey().isEmpty() + && deviceActivationRequest().pniPqLastResortPreKey().isEmpty(); return supportsAtomicAccountCreation() || hasNoAtomicAccountCreationParameters; } @@ -140,18 +123,18 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT return hasExactlyOneMessageDeliveryChannel() && aciIdentityKey().isPresent() && pniIdentityKey().isPresent() - && aciSignedPreKey().isPresent() - && pniSignedPreKey().isPresent() - && aciPqLastResortPreKey().isPresent() - && pniPqLastResortPreKey().isPresent(); + && deviceActivationRequest().aciSignedPreKey().isPresent() + && deviceActivationRequest().pniSignedPreKey().isPresent() + && deviceActivationRequest().aciPqLastResortPreKey().isPresent() + && deviceActivationRequest().pniPqLastResortPreKey().isPresent(); } @VisibleForTesting boolean hasExactlyOneMessageDeliveryChannel() { if (accountAttributes.getFetchesMessages()) { - return apnToken.isEmpty() && gcmToken.isEmpty(); + return deviceActivationRequest().apnToken().isEmpty() && deviceActivationRequest().gcmToken().isEmpty(); } else { - return apnToken.isPresent() ^ gcmToken.isPresent(); + return deviceActivationRequest().apnToken().isPresent() ^ deviceActivationRequest().gcmToken().isPresent(); } } }