mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 05:48:04 +01:00
Always require atomic account creation
This commit is contained in:
committed by
Jon Chambers
parent
9069c5abb6
commit
521900c048
@@ -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<String, Integer> maxDeviceConfiguration;
|
||||
private final Map<String, Integer> 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<Account, Device> 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<Account, Device> createDevice(final String password,
|
||||
final String verificationCode,
|
||||
final AccountAttributes accountAttributes,
|
||||
final ContainerRequest containerRequest,
|
||||
final Optional<DeviceActivationRequest> maybeDeviceActivationRequest)
|
||||
final String verificationCode,
|
||||
final AccountAttributes accountAttributes,
|
||||
final ContainerRequest containerRequest,
|
||||
final Optional<DeviceActivationRequest> maybeDeviceActivationRequest)
|
||||
throws RateLimitExceededException, DeviceLimitExceededException {
|
||||
|
||||
final Optional<UUID> 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);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<Optional<IdentityKey>> {
|
||||
|
||||
@Override
|
||||
public void serialize(final Optional<IdentityKey> 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<Optional<IdentityKey>> {
|
||||
|
||||
@Override
|
||||
public Optional<IdentityKey> 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<IdentityKey> getNullValue(DeserializationContext ctxt) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user