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.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()));
});
}

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;
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<String> 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<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 = """
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();
}
}
}