Factor `DeviceActivationRequest` out into its own record

This commit is contained in:
Jon Chambers 2023-05-18 10:54:53 -04:00 committed by Jon Chambers
parent 1a5327aece
commit ae7cb8036e
3 changed files with 104 additions and 69 deletions

View File

@ -21,7 +21,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import javax.validation.Valid; import javax.validation.Valid;
@ -95,7 +94,7 @@ public class RegistrationController {
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Registers an account", @Operation(summary = "Registers an account",
description = """ 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: could make up to three consecutive calls to this API:
1. gets 423 from existing registration lock \n 1. gets 423 from existing registration lock \n
2. gets 409 from device available for transfer \n 2. gets 409 from device available for transfer \n
@ -150,10 +149,10 @@ public class RegistrationController {
if (registrationRequest.supportsAtomicAccountCreation()) { if (registrationRequest.supportsAtomicAccountCreation()) {
assert registrationRequest.aciIdentityKey().isPresent(); assert registrationRequest.aciIdentityKey().isPresent();
assert registrationRequest.pniIdentityKey().isPresent(); assert registrationRequest.pniIdentityKey().isPresent();
assert registrationRequest.aciSignedPreKey().isPresent(); assert registrationRequest.deviceActivationRequest().aciSignedPreKey().isPresent();
assert registrationRequest.pniSignedPreKey().isPresent(); assert registrationRequest.deviceActivationRequest().pniSignedPreKey().isPresent();
assert registrationRequest.aciPqLastResortPreKey().isPresent(); assert registrationRequest.deviceActivationRequest().aciPqLastResortPreKey().isPresent();
assert registrationRequest.pniPqLastResortPreKey().isPresent(); assert registrationRequest.deviceActivationRequest().pniPqLastResortPreKey().isPresent();
account = accounts.update(account, a -> { account = accounts.update(account, a -> {
a.setIdentityKey(registrationRequest.aciIdentityKey().get()); a.setIdentityKey(registrationRequest.aciIdentityKey().get());
@ -161,19 +160,19 @@ public class RegistrationController {
final Device device = a.getMasterDevice().orElseThrow(); final Device device = a.getMasterDevice().orElseThrow();
device.setSignedPreKey(registrationRequest.aciSignedPreKey().get()); device.setSignedPreKey(registrationRequest.deviceActivationRequest().aciSignedPreKey().get());
device.setPhoneNumberIdentitySignedPreKey(registrationRequest.pniSignedPreKey().get()); device.setPhoneNumberIdentitySignedPreKey(registrationRequest.deviceActivationRequest().pniSignedPreKey().get());
registrationRequest.apnToken().ifPresent(apnRegistrationId -> { registrationRequest.deviceActivationRequest().apnToken().ifPresent(apnRegistrationId -> {
device.setApnId(apnRegistrationId.apnRegistrationId()); device.setApnId(apnRegistrationId.apnRegistrationId());
device.setVoipApnId(apnRegistrationId.voipRegistrationId()); device.setVoipApnId(apnRegistrationId.voipRegistrationId());
}); });
registrationRequest.gcmToken().ifPresent(gcmRegistrationId -> registrationRequest.deviceActivationRequest().gcmToken().ifPresent(gcmRegistrationId ->
device.setGcmId(gcmRegistrationId.gcmRegistrationId())); device.setGcmId(gcmRegistrationId.gcmRegistrationId()));
keys.storePqLastResort(a.getUuid(), Map.of(Device.MASTER_ID, registrationRequest.aciPqLastResortPreKey().get())); keys.storePqLastResort(a.getUuid(), Map.of(Device.MASTER_ID, registrationRequest.deviceActivationRequest().aciPqLastResortPreKey().get()));
keys.storePqLastResort(a.getPhoneNumberIdentifier(), Map.of(Device.MASTER_ID, registrationRequest.pniPqLastResortPreKey().get())); keys.storePqLastResort(a.getPhoneNumberIdentifier(), Map.of(Device.MASTER_ID, registrationRequest.deviceActivationRequest().pniPqLastResortPreKey().get()));
}); });
} }

View File

@ -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) {
}

View File

@ -5,6 +5,9 @@
package org.whispersystems.textsecuregcm.entities; 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.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@ -59,58 +62,38 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
""") """)
Optional<String> pniIdentityKey, Optional<String> pniIdentityKey,
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ @JsonUnwrapped
A signed EC pre-key to be associated with this account's ACI. If provided, an account @JsonProperty(access = JsonProperty.Access.READ_ONLY)
will be created "atomically," and all other properties needed for atomic account DeviceActivationRequest deviceActivationRequest) implements PhoneVerificationRequest {
creation must also be present.
""")
Optional<@Valid SignedPreKey> aciSignedPreKey,
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """ @JsonCreator
A signed EC pre-key to be associated with this account's PNI. If provided, an account @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
will be created "atomically," and all other properties needed for atomic account public RegistrationRequest(@JsonProperty("sessionId") String sessionId,
creation must also be present. @JsonProperty("recoveryPassword") byte[] recoveryPassword,
""") @JsonProperty("accountAttributes") AccountAttributes accountAttributes,
Optional<@Valid SignedPreKey> pniSignedPreKey, @JsonProperty("skipDeviceTransfer") boolean skipDeviceTransfer,
@JsonProperty("aciIdentityKey") Optional<String> aciIdentityKey,
@JsonProperty("pniIdentityKey") Optional<String> 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 = """ // This may seem a little verbose, but at the time of writing, Jackson struggles with `@JsonUnwrapped` members in
A signed Kyber-1024 "last resort" pre-key to be associated with this account's ACI. If // records, and this is a workaround. Please see
provided, an account will be created "atomically," and all other properties needed for // https://github.com/FasterXML/jackson-databind/issues/3726#issuecomment-1525396869 for additional context.
atomic account creation must also be present. this(sessionId, recoveryPassword, accountAttributes, skipDeviceTransfer, aciIdentityKey, pniIdentityKey,
""") new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnToken, gcmToken));
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 {
@AssertTrue @AssertTrue
public boolean isEverySignedKeyValid() { public boolean isEverySignedKeyValid() {
return validatePreKeySignature(aciIdentityKey(), aciSignedPreKey()) return validatePreKeySignature(aciIdentityKey(), deviceActivationRequest().aciSignedPreKey())
&& validatePreKeySignature(pniIdentityKey(), pniSignedPreKey()) && validatePreKeySignature(pniIdentityKey(), deviceActivationRequest().pniSignedPreKey())
&& validatePreKeySignature(aciIdentityKey(), aciPqLastResortPreKey()) && validatePreKeySignature(aciIdentityKey(), deviceActivationRequest().aciPqLastResortPreKey())
&& validatePreKeySignature(pniIdentityKey(), pniPqLastResortPreKey()); && validatePreKeySignature(pniIdentityKey(), deviceActivationRequest().pniPqLastResortPreKey());
} }
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@ -128,10 +111,10 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
final boolean hasNoAtomicAccountCreationParameters = final boolean hasNoAtomicAccountCreationParameters =
aciIdentityKey().isEmpty() aciIdentityKey().isEmpty()
&& pniIdentityKey().isEmpty() && pniIdentityKey().isEmpty()
&& aciSignedPreKey().isEmpty() && deviceActivationRequest().aciSignedPreKey().isEmpty()
&& pniSignedPreKey().isEmpty() && deviceActivationRequest().pniSignedPreKey().isEmpty()
&& aciPqLastResortPreKey().isEmpty() && deviceActivationRequest().aciPqLastResortPreKey().isEmpty()
&& pniPqLastResortPreKey().isEmpty(); && deviceActivationRequest().pniPqLastResortPreKey().isEmpty();
return supportsAtomicAccountCreation() || hasNoAtomicAccountCreationParameters; return supportsAtomicAccountCreation() || hasNoAtomicAccountCreationParameters;
} }
@ -140,18 +123,18 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
return hasExactlyOneMessageDeliveryChannel() return hasExactlyOneMessageDeliveryChannel()
&& aciIdentityKey().isPresent() && aciIdentityKey().isPresent()
&& pniIdentityKey().isPresent() && pniIdentityKey().isPresent()
&& aciSignedPreKey().isPresent() && deviceActivationRequest().aciSignedPreKey().isPresent()
&& pniSignedPreKey().isPresent() && deviceActivationRequest().pniSignedPreKey().isPresent()
&& aciPqLastResortPreKey().isPresent() && deviceActivationRequest().aciPqLastResortPreKey().isPresent()
&& pniPqLastResortPreKey().isPresent(); && deviceActivationRequest().pniPqLastResortPreKey().isPresent();
} }
@VisibleForTesting @VisibleForTesting
boolean hasExactlyOneMessageDeliveryChannel() { boolean hasExactlyOneMessageDeliveryChannel() {
if (accountAttributes.getFetchesMessages()) { if (accountAttributes.getFetchesMessages()) {
return apnToken.isEmpty() && gcmToken.isEmpty(); return deviceActivationRequest().apnToken().isEmpty() && deviceActivationRequest().gcmToken().isEmpty();
} else { } else {
return apnToken.isPresent() ^ gcmToken.isPresent(); return deviceActivationRequest().apnToken().isPresent() ^ deviceActivationRequest().gcmToken().isPresent();
} }
} }
} }