mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 21:08:07 +01:00
Always require atomic account creation
This commit is contained in:
committed by
Jon Chambers
parent
9069c5abb6
commit
521900c048
@@ -122,4 +122,9 @@ public class AccountAttributes {
|
||||
this.recoveryPassword = recoveryPassword;
|
||||
return this;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setPhoneNumberIdentityRegistrationId(final Integer phoneNumberIdentityRegistrationId) {
|
||||
this.phoneNumberIdentityRegistrationId = phoneNumberIdentityRegistrationId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,52 +3,52 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record DeviceActivationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed EC pre-key to be associated with this account's ACI. If provided, an account
|
||||
will be created "atomically," and all other properties needed for atomic account
|
||||
creation must also be present.
|
||||
""")
|
||||
Optional<@Valid ECSignedPreKey> aciSignedPreKey,
|
||||
public record DeviceActivationRequest(
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
A signed EC pre-key to be associated with this account's ACI.
|
||||
""")
|
||||
ECSignedPreKey aciSignedPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed EC pre-key to be associated with this account's PNI. If provided, an account
|
||||
will be created "atomically," and all other properties needed for atomic account
|
||||
creation must also be present.
|
||||
""")
|
||||
Optional<@Valid ECSignedPreKey> pniSignedPreKey,
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
A signed EC pre-key to be associated with this account's PNI.
|
||||
""")
|
||||
ECSignedPreKey pniSignedPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed Kyber-1024 "last resort" pre-key to be associated with this account's ACI. If
|
||||
provided, an account will be created "atomically," and all other properties needed for
|
||||
atomic account creation must also be present.
|
||||
""")
|
||||
Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
A signed Kyber-1024 "last resort" pre-key to be associated with this account's ACI.
|
||||
""")
|
||||
KEMSignedPreKey aciPqLastResortPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed Kyber-1024 "last resort" pre-key to be associated with this account's PNI. If
|
||||
provided, an account will be created "atomically," and all other properties needed for
|
||||
atomic account creation must also be present.
|
||||
""")
|
||||
Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey,
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
A signed Kyber-1024 "last resort" pre-key to be associated with this account's PNI.
|
||||
""")
|
||||
KEMSignedPreKey pniPqLastResortPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
An APNs token set for the account's primary device. If provided, the account's primary
|
||||
device will be notified of new messages via push notifications to the given token. If
|
||||
creating an account "atomically," callers must provide exactly one of an APNs token
|
||||
set, an FCM token, or an `AccountAttributes` entity with `fetchesMessages` set to
|
||||
`true`.
|
||||
""")
|
||||
Optional<@Valid ApnRegistrationId> apnToken,
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
An APNs token set for the account's primary device. If provided, the account's primary
|
||||
device will be notified of new messages via push notifications to the given token.
|
||||
Callers must provide exactly one of an APNs token set, an FCM token, or an
|
||||
`AccountAttributes` entity with `fetchesMessages` set to `true`.
|
||||
""")
|
||||
Optional<@Valid ApnRegistrationId> apnToken,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
An FCM/GCM token for the account's primary device. If provided, the account's primary
|
||||
device will be notified of new messages via push notifications to the given token. If
|
||||
creating an account "atomically," callers must provide exactly one of an APNs token
|
||||
set, an FCM token, or an `AccountAttributes` entity with `fetchesMessages` set to
|
||||
`true`.
|
||||
""")
|
||||
Optional<@Valid GcmRegistrationId> gcmToken) {
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
An FCM/GCM token for the account's primary device. If provided, the account's primary
|
||||
device will be notified of new messages via push notifications to the given token.
|
||||
Callers must provide exactly one of an APNs token set, an FCM token, or an
|
||||
`AccountAttributes` entity with `fetchesMessages` set to `true`.
|
||||
""")
|
||||
Optional<@Valid GcmRegistrationId> gcmToken) {
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Optional;
|
||||
|
||||
public record LinkDeviceRequest(@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
@@ -17,6 +18,8 @@ public record LinkDeviceRequest(@Schema(requiredMode = Schema.RequiredMode.REQUI
|
||||
|
||||
AccountAttributes accountAttributes,
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonUnwrapped
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
DeviceActivationRequest deviceActivationRequest) {
|
||||
@@ -25,10 +28,10 @@ public record LinkDeviceRequest(@Schema(requiredMode = Schema.RequiredMode.REQUI
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public LinkDeviceRequest(@JsonProperty("verificationCode") String verificationCode,
|
||||
@JsonProperty("accountAttributes") AccountAttributes accountAttributes,
|
||||
@JsonProperty("aciSignedPreKey") Optional<@Valid ECSignedPreKey> aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") Optional<@Valid ECSignedPreKey> pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey,
|
||||
@JsonProperty("aciSignedPreKey") @NotNull @Valid ECSignedPreKey aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") @NotNull @Valid ECSignedPreKey pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") @NotNull @Valid KEMSignedPreKey aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") @NotNull @Valid KEMSignedPreKey pniPqLastResortPreKey,
|
||||
@JsonProperty("apnToken") Optional<@Valid ApnRegistrationId> apnToken,
|
||||
@JsonProperty("gcmToken") Optional<@Valid GcmRegistrationId> gcmToken) {
|
||||
|
||||
@@ -36,14 +39,6 @@ public record LinkDeviceRequest(@Schema(requiredMode = Schema.RequiredMode.REQUI
|
||||
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnToken, gcmToken));
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
public boolean hasAllRequiredFields() {
|
||||
return deviceActivationRequest().aciSignedPreKey().isPresent()
|
||||
&& deviceActivationRequest().pniSignedPreKey().isPresent()
|
||||
&& deviceActivationRequest().aciPqLastResortPreKey().isPresent()
|
||||
&& deviceActivationRequest().pniPqLastResortPreKey().isPresent();
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
public boolean hasExactlyOneMessageDeliveryChannel() {
|
||||
if (accountAttributes.getFetchesMessages()) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.OptionalIdentityKeyAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
|
||||
|
||||
public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
The ID of an existing verification session as it appears in a verification session
|
||||
@@ -50,31 +50,26 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
|
||||
""")
|
||||
boolean skipDeviceTransfer,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
If true, indicates that this is a request for "atomic" registration. If any properties
|
||||
needed for atomic account creation are not present, the request will fail. If false,
|
||||
atomic account creation can still occur, but only if all required fields are present.
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
The ACI-associated identity key for the account, encoded as a base64 string.
|
||||
""")
|
||||
boolean requireAtomic,
|
||||
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
|
||||
IdentityKey aciIdentityKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
The ACI-associated identity key for the account, encoded as a base64 string. If
|
||||
provided, an account will be created "atomically," and all other properties needed for
|
||||
atomic account creation must also be present.
|
||||
@NotNull
|
||||
@Valid
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = """
|
||||
The PNI-associated identity key for the account, encoded as a base64 string.
|
||||
""")
|
||||
@JsonSerialize(using = OptionalIdentityKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = OptionalIdentityKeyAdapter.Deserializer.class)
|
||||
Optional<IdentityKey> aciIdentityKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
The PNI-associated identity key for the account, encoded as a base64 string. If
|
||||
provided, an account will be created "atomically," and all other properties needed for
|
||||
atomic account creation must also be present.
|
||||
""")
|
||||
@JsonSerialize(using = OptionalIdentityKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = OptionalIdentityKeyAdapter.Deserializer.class)
|
||||
Optional<IdentityKey> pniIdentityKey,
|
||||
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
|
||||
IdentityKey pniIdentityKey,
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@JsonUnwrapped
|
||||
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
|
||||
DeviceActivationRequest deviceActivationRequest) implements PhoneVerificationRequest {
|
||||
@@ -85,65 +80,37 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
|
||||
@JsonProperty("recoveryPassword") byte[] recoveryPassword,
|
||||
@JsonProperty("accountAttributes") AccountAttributes accountAttributes,
|
||||
@JsonProperty("skipDeviceTransfer") boolean skipDeviceTransfer,
|
||||
@JsonProperty("requireAtomic") boolean requireAtomic,
|
||||
@JsonProperty("aciIdentityKey") Optional<IdentityKey> aciIdentityKey,
|
||||
@JsonProperty("pniIdentityKey") Optional<IdentityKey> pniIdentityKey,
|
||||
@JsonProperty("aciSignedPreKey") Optional<@Valid ECSignedPreKey> aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") Optional<@Valid ECSignedPreKey> pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey,
|
||||
@JsonProperty("aciIdentityKey") @NotNull @Valid IdentityKey aciIdentityKey,
|
||||
@JsonProperty("pniIdentityKey") @NotNull @Valid IdentityKey pniIdentityKey,
|
||||
@JsonProperty("aciSignedPreKey") @NotNull @Valid ECSignedPreKey aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") @NotNull @Valid ECSignedPreKey pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") @NotNull @Valid KEMSignedPreKey aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") @NotNull @Valid KEMSignedPreKey pniPqLastResortPreKey,
|
||||
@JsonProperty("apnToken") Optional<@Valid ApnRegistrationId> apnToken,
|
||||
@JsonProperty("gcmToken") Optional<@Valid GcmRegistrationId> gcmToken) {
|
||||
|
||||
// This may seem a little verbose, but at the time of writing, Jackson struggles with `@JsonUnwrapped` members in
|
||||
// records, and this is a workaround. Please see
|
||||
// https://github.com/FasterXML/jackson-databind/issues/3726#issuecomment-1525396869 for additional context.
|
||||
this(sessionId, recoveryPassword, accountAttributes, skipDeviceTransfer, requireAtomic, aciIdentityKey, pniIdentityKey,
|
||||
this(sessionId, recoveryPassword, accountAttributes, skipDeviceTransfer, aciIdentityKey, pniIdentityKey,
|
||||
new DeviceActivationRequest(aciSignedPreKey, pniSignedPreKey, aciPqLastResortPreKey, pniPqLastResortPreKey, apnToken, gcmToken));
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
public boolean isEverySignedKeyValid() {
|
||||
return validatePreKeySignature(aciIdentityKey(), deviceActivationRequest().aciSignedPreKey())
|
||||
&& validatePreKeySignature(pniIdentityKey(), deviceActivationRequest().pniSignedPreKey())
|
||||
&& validatePreKeySignature(aciIdentityKey(), deviceActivationRequest().aciPqLastResortPreKey())
|
||||
&& validatePreKeySignature(pniIdentityKey(), deviceActivationRequest().pniPqLastResortPreKey());
|
||||
}
|
||||
if (deviceActivationRequest().aciSignedPreKey() == null ||
|
||||
deviceActivationRequest().pniSignedPreKey() == null ||
|
||||
deviceActivationRequest().aciPqLastResortPreKey() == null ||
|
||||
deviceActivationRequest().pniPqLastResortPreKey() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private static boolean validatePreKeySignature(final Optional<IdentityKey> maybeIdentityKey,
|
||||
final Optional<? extends SignedPreKey<?>> maybeSignedPreKey) {
|
||||
|
||||
return maybeSignedPreKey.map(signedPreKey -> maybeIdentityKey
|
||||
.map(identityKey -> PreKeySignatureValidator.validatePreKeySignatures(identityKey, List.of(signedPreKey)))
|
||||
.orElse(false))
|
||||
.orElse(true);
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
public boolean isCompleteRequest() {
|
||||
final boolean hasNoAtomicAccountCreationParameters =
|
||||
aciIdentityKey().isEmpty()
|
||||
&& pniIdentityKey().isEmpty()
|
||||
&& deviceActivationRequest().aciSignedPreKey().isEmpty()
|
||||
&& deviceActivationRequest().pniSignedPreKey().isEmpty()
|
||||
&& deviceActivationRequest().aciPqLastResortPreKey().isEmpty()
|
||||
&& deviceActivationRequest().pniPqLastResortPreKey().isEmpty();
|
||||
|
||||
return supportsAtomicAccountCreation() || (!requireAtomic() && hasNoAtomicAccountCreationParameters);
|
||||
}
|
||||
|
||||
public boolean supportsAtomicAccountCreation() {
|
||||
return hasExactlyOneMessageDeliveryChannel()
|
||||
&& aciIdentityKey().isPresent()
|
||||
&& pniIdentityKey().isPresent()
|
||||
&& deviceActivationRequest().aciSignedPreKey().isPresent()
|
||||
&& deviceActivationRequest().pniSignedPreKey().isPresent()
|
||||
&& deviceActivationRequest().aciPqLastResortPreKey().isPresent()
|
||||
&& deviceActivationRequest().pniPqLastResortPreKey().isPresent();
|
||||
return PreKeySignatureValidator.validatePreKeySignatures(aciIdentityKey(), List.of(deviceActivationRequest().aciSignedPreKey(), deviceActivationRequest().aciPqLastResortPreKey()))
|
||||
&& PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey(), List.of(deviceActivationRequest().pniSignedPreKey(), deviceActivationRequest().pniPqLastResortPreKey()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@AssertTrue
|
||||
boolean hasExactlyOneMessageDeliveryChannel() {
|
||||
if (accountAttributes.getFetchesMessages()) {
|
||||
return deviceActivationRequest().apnToken().isEmpty() && deviceActivationRequest().gcmToken().isEmpty();
|
||||
|
||||
Reference in New Issue
Block a user