mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 02:08:03 +01:00
Introduce "service identifiers"
This commit is contained in:
@@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.controllers;
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
@@ -50,6 +51,8 @@ import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.UsernameLinkHandle;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
@@ -399,6 +402,7 @@ public class AccountController {
|
||||
return accounts
|
||||
.getByUsernameHash(hash)
|
||||
.map(Account::getUuid)
|
||||
.map(AciServiceIdentifier::new)
|
||||
.map(AccountIdentifierResponse::new)
|
||||
.orElseThrow(() -> new WebApplicationException(Status.NOT_FOUND));
|
||||
}
|
||||
@@ -485,21 +489,32 @@ public class AccountController {
|
||||
return new EncryptedUsername(maybeEncryptedUsername.get());
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Check whether an account exists",
|
||||
description = """
|
||||
Enforced unauthenticated endpoint. Checks whether an account with a given identifier exists.
|
||||
"""
|
||||
)
|
||||
@ApiResponse(responseCode = "200", description = "An account with the given identifier was found.", useReturnTypeSchema = true)
|
||||
@ApiResponse(responseCode = "400", description = "A client made an authenticated to this endpoint, and must not provide credentials.")
|
||||
@ApiResponse(responseCode = "404", description = "An account was not found for the given identifier.")
|
||||
@ApiResponse(responseCode = "422", description = "Invalid request format.")
|
||||
@ApiResponse(responseCode = "429", description = "Rate-limited.")
|
||||
@HEAD
|
||||
@Path("/account/{uuid}")
|
||||
@Path("/account/{identifier}")
|
||||
@RateLimitedByIp(RateLimiters.For.CHECK_ACCOUNT_EXISTENCE)
|
||||
public Response accountExists(
|
||||
@Auth final Optional<AuthenticatedAccount> authenticatedAccount,
|
||||
@PathParam("uuid") final UUID uuid) throws RateLimitExceededException {
|
||||
|
||||
@Parameter(description = "An ACI or PNI account identifier to check")
|
||||
@PathParam("identifier") final ServiceIdentifier accountIdentifier) {
|
||||
|
||||
// Disallow clients from making authenticated requests to this endpoint
|
||||
requireNotAuthenticated(authenticatedAccount);
|
||||
|
||||
final Status status = accounts.getByAccountIdentifier(uuid)
|
||||
.or(() -> accounts.getByPhoneNumberIdentifier(uuid))
|
||||
.isPresent() ? Status.OK : Status.NOT_FOUND;
|
||||
final Optional<Account> maybeAccount = accounts.getByServiceIdentifier(accountIdentifier);
|
||||
|
||||
return Response.status(status).build();
|
||||
return Response.status(maybeAccount.map(ignored -> Status.OK).orElse(Status.NOT_FOUND)).build();
|
||||
}
|
||||
|
||||
@Timed
|
||||
|
||||
@@ -57,6 +57,7 @@ import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyState;
|
||||
import org.whispersystems.textsecuregcm.experiment.Experiment;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
@@ -207,7 +208,7 @@ public class KeysController {
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
|
||||
@Parameter(description="the account or phone-number identifier to retrieve keys for")
|
||||
@PathParam("identifier") UUID targetUuid,
|
||||
@PathParam("identifier") ServiceIdentifier targetIdentifier,
|
||||
|
||||
@Parameter(description="the device id of a single device to retrieve prekeys for, or `*` for all enabled devices")
|
||||
@PathParam("device_id") String deviceId,
|
||||
@@ -227,8 +228,7 @@ public class KeysController {
|
||||
|
||||
final Account target;
|
||||
{
|
||||
final Optional<Account> maybeTarget = accounts.getByAccountIdentifier(targetUuid)
|
||||
.or(() -> accounts.getByPhoneNumberIdentifier(targetUuid));
|
||||
final Optional<Account> maybeTarget = accounts.getByServiceIdentifier(targetIdentifier);
|
||||
|
||||
OptionalAccess.verify(account, accessKey, maybeTarget, deviceId);
|
||||
|
||||
@@ -237,34 +237,39 @@ public class KeysController {
|
||||
|
||||
if (account.isPresent()) {
|
||||
rateLimiters.getPreKeysLimiter().validate(
|
||||
account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + targetUuid
|
||||
account.get().getUuid() + "." + auth.get().getAuthenticatedDevice().getId() + "__" + targetIdentifier.uuid()
|
||||
+ "." + deviceId);
|
||||
}
|
||||
|
||||
final boolean usePhoneNumberIdentity = target.getPhoneNumberIdentifier().equals(targetUuid);
|
||||
|
||||
List<Device> devices = parseDeviceId(deviceId, target);
|
||||
List<PreKeyResponseItem> responseItems = new ArrayList<>(devices.size());
|
||||
|
||||
for (Device device : devices) {
|
||||
UUID identifier = usePhoneNumberIdentity ? target.getPhoneNumberIdentifier() : targetUuid;
|
||||
ECSignedPreKey signedECPreKey = usePhoneNumberIdentity ? device.getPhoneNumberIdentitySignedPreKey() : device.getSignedPreKey();
|
||||
ECPreKey unsignedECPreKey = keys.takeEC(identifier, device.getId()).join().orElse(null);
|
||||
KEMSignedPreKey pqPreKey = returnPqKey ? keys.takePQ(identifier, device.getId()).join().orElse(null) : null;
|
||||
ECSignedPreKey signedECPreKey = switch (targetIdentifier.identityType()) {
|
||||
case ACI -> device.getSignedPreKey();
|
||||
case PNI -> device.getPhoneNumberIdentitySignedPreKey();
|
||||
};
|
||||
|
||||
ECPreKey unsignedECPreKey = keys.takeEC(targetIdentifier.uuid(), device.getId()).join().orElse(null);
|
||||
KEMSignedPreKey pqPreKey = returnPqKey ? keys.takePQ(targetIdentifier.uuid(), device.getId()).join().orElse(null) : null;
|
||||
|
||||
compareSignedEcPreKeysExperiment.compareFutureResult(Optional.ofNullable(signedECPreKey),
|
||||
keys.getEcSignedPreKey(identifier, device.getId()));
|
||||
keys.getEcSignedPreKey(targetIdentifier.uuid(), device.getId()));
|
||||
|
||||
if (signedECPreKey != null || unsignedECPreKey != null || pqPreKey != null) {
|
||||
final int registrationId = usePhoneNumberIdentity ?
|
||||
device.getPhoneNumberIdentityRegistrationId().orElse(device.getRegistrationId()) :
|
||||
device.getRegistrationId();
|
||||
final int registrationId = switch (targetIdentifier.identityType()) {
|
||||
case ACI -> device.getRegistrationId();
|
||||
case PNI -> device.getPhoneNumberIdentityRegistrationId().orElse(device.getRegistrationId());
|
||||
};
|
||||
|
||||
responseItems.add(new PreKeyResponseItem(device.getId(), registrationId, signedECPreKey, unsignedECPreKey, pqPreKey));
|
||||
}
|
||||
}
|
||||
|
||||
final IdentityKey identityKey = usePhoneNumberIdentity ? target.getPhoneNumberIdentityKey() : target.getIdentityKey();
|
||||
final IdentityKey identityKey = switch (targetIdentifier.identityType()) {
|
||||
case ACI -> target.getIdentityKey();
|
||||
case PNI -> target.getPhoneNumberIdentityKey();
|
||||
};
|
||||
|
||||
if (responseItems.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.NOT_FOUND);
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -35,7 +36,6 @@ import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.Nonnull;
|
||||
@@ -48,6 +48,7 @@ import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
@@ -82,6 +83,8 @@ import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.SendMultiRecipientMessageResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.SpamReport;
|
||||
import org.whispersystems.textsecuregcm.entities.StaleDevices;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.MessageMetrics;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
@@ -183,7 +186,7 @@ public class MessageController {
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent,
|
||||
@HeaderParam(HttpHeaders.X_FORWARDED_FOR) String forwardedFor,
|
||||
@PathParam("destination") UUID destinationUuid,
|
||||
@PathParam("destination") ServiceIdentifier destinationIdentifier,
|
||||
@QueryParam("story") boolean isStory,
|
||||
@NotNull @Valid IncomingMessageList messages,
|
||||
@Context ContainerRequestContext context) throws RateLimitExceededException {
|
||||
@@ -195,7 +198,7 @@ public class MessageController {
|
||||
final String senderType;
|
||||
|
||||
if (source.isPresent()) {
|
||||
if (source.get().getAccount().isIdentifiedBy(destinationUuid)) {
|
||||
if (source.get().getAccount().isIdentifiedBy(destinationIdentifier)) {
|
||||
senderType = SENDER_TYPE_SELF;
|
||||
} else {
|
||||
senderType = SENDER_TYPE_IDENTIFIED;
|
||||
@@ -227,7 +230,7 @@ public class MessageController {
|
||||
}
|
||||
|
||||
try {
|
||||
rateLimiters.getInboundMessageBytes().validate(destinationUuid, totalContentLength);
|
||||
rateLimiters.getInboundMessageBytes().validate(destinationIdentifier.uuid(), totalContentLength);
|
||||
} catch (final RateLimitExceededException e) {
|
||||
if (dynamicConfigurationManager.getConfiguration().getInboundMessageByteLimitConfiguration().enforceInboundLimit()) {
|
||||
throw e;
|
||||
@@ -235,13 +238,12 @@ public class MessageController {
|
||||
}
|
||||
|
||||
try {
|
||||
boolean isSyncMessage = source.isPresent() && source.get().getAccount().isIdentifiedBy(destinationUuid);
|
||||
boolean isSyncMessage = source.isPresent() && source.get().getAccount().isIdentifiedBy(destinationIdentifier);
|
||||
|
||||
Optional<Account> destination;
|
||||
|
||||
if (!isSyncMessage) {
|
||||
destination = accountsManager.getByAccountIdentifier(destinationUuid)
|
||||
.or(() -> accountsManager.getByPhoneNumberIdentifier(destinationUuid));
|
||||
destination = accountsManager.getByServiceIdentifier(destinationIdentifier);
|
||||
} else {
|
||||
destination = source.map(AuthenticatedAccount::getAccount);
|
||||
}
|
||||
@@ -288,7 +290,7 @@ public class MessageController {
|
||||
messages.messages(),
|
||||
IncomingMessage::destinationDeviceId,
|
||||
IncomingMessage::destinationRegistrationId,
|
||||
destination.get().getPhoneNumberIdentifier().equals(destinationUuid));
|
||||
destination.get().getPhoneNumberIdentifier().equals(destinationIdentifier.uuid()));
|
||||
|
||||
final List<Tag> tags = List.of(UserAgentTagUtil.getPlatformTag(userAgent),
|
||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(messages.online())),
|
||||
@@ -303,7 +305,7 @@ public class MessageController {
|
||||
source,
|
||||
destination.get(),
|
||||
destinationDevice.get(),
|
||||
destinationUuid,
|
||||
destinationIdentifier,
|
||||
messages.timestamp(),
|
||||
messages.online(),
|
||||
isStory,
|
||||
@@ -334,25 +336,20 @@ public class MessageController {
|
||||
|
||||
/**
|
||||
* Build mapping of accounts to devices/registration IDs.
|
||||
*
|
||||
* @param multiRecipientMessage
|
||||
* @param uuidToAccountMap
|
||||
* @return
|
||||
*/
|
||||
private Map<Account, Set<Pair<Long, Integer>>> buildDeviceIdAndRegistrationIdMap(
|
||||
MultiRecipientMessage multiRecipientMessage,
|
||||
Map<UUID, Account> uuidToAccountMap
|
||||
) {
|
||||
Map<ServiceIdentifier, Account> accountsByServiceIdentifier) {
|
||||
|
||||
return Arrays.stream(multiRecipientMessage.getRecipients())
|
||||
return Arrays.stream(multiRecipientMessage.recipients())
|
||||
// for normal messages, all recipients UUIDs are in the map,
|
||||
// but story messages might specify inactive UUIDs, which we
|
||||
// have previously filtered
|
||||
.filter(r -> uuidToAccountMap.containsKey(r.getUuid()))
|
||||
.filter(r -> accountsByServiceIdentifier.containsKey(r.uuid()))
|
||||
.collect(Collectors.toMap(
|
||||
recipient -> uuidToAccountMap.get(recipient.getUuid()),
|
||||
recipient -> accountsByServiceIdentifier.get(recipient.uuid()),
|
||||
recipient -> new HashSet<>(
|
||||
Collections.singletonList(new Pair<>(recipient.getDeviceId(), recipient.getRegistrationId()))),
|
||||
Collections.singletonList(new Pair<>(recipient.deviceId(), recipient.registrationId()))),
|
||||
(a, b) -> {
|
||||
a.addAll(b);
|
||||
return a;
|
||||
@@ -376,33 +373,29 @@ public class MessageController {
|
||||
@QueryParam("story") boolean isStory,
|
||||
@NotNull @Valid MultiRecipientMessage multiRecipientMessage) {
|
||||
|
||||
// we skip "missing" accounts when story=true.
|
||||
// otherwise, we return a 404 status code.
|
||||
final Function<UUID, Stream<Account>> accountFinder = uuid -> {
|
||||
Optional<Account> res = accountsManager.getByAccountIdentifier(uuid);
|
||||
if (!isStory && res.isEmpty()) {
|
||||
throw new WebApplicationException(Status.NOT_FOUND);
|
||||
}
|
||||
return res.stream();
|
||||
};
|
||||
final Map<ServiceIdentifier, Account> accountsByServiceIdentifier = new HashMap<>();
|
||||
|
||||
// build a map from UUID to accounts
|
||||
Map<UUID, Account> uuidToAccountMap =
|
||||
Arrays.stream(multiRecipientMessage.getRecipients())
|
||||
.map(Recipient::getUuid)
|
||||
.distinct()
|
||||
.flatMap(accountFinder)
|
||||
.collect(Collectors.toUnmodifiableMap(
|
||||
Account::getUuid,
|
||||
Function.identity()));
|
||||
for (final Recipient recipient : multiRecipientMessage.recipients()) {
|
||||
if (!accountsByServiceIdentifier.containsKey(recipient.uuid())) {
|
||||
final Optional<Account> maybeAccount = accountsManager.getByServiceIdentifier(recipient.uuid());
|
||||
|
||||
if (maybeAccount.isPresent()) {
|
||||
accountsByServiceIdentifier.put(recipient.uuid(), maybeAccount.get());
|
||||
} else {
|
||||
if (!isStory) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stories will be checked by the client; we bypass access checks here for stories.
|
||||
if (!isStory) {
|
||||
checkAccessKeys(accessKeys, uuidToAccountMap);
|
||||
checkAccessKeys(accessKeys, accountsByServiceIdentifier.values());
|
||||
}
|
||||
|
||||
final Map<Account, Set<Pair<Long, Integer>>> accountToDeviceIdAndRegistrationIdMap =
|
||||
buildDeviceIdAndRegistrationIdMap(multiRecipientMessage, uuidToAccountMap);
|
||||
buildDeviceIdAndRegistrationIdMap(multiRecipientMessage, accountsByServiceIdentifier);
|
||||
|
||||
// We might filter out all the recipients of a story (if none have enabled stories).
|
||||
// In this case there is no error so we should just return 200 now.
|
||||
@@ -412,7 +405,7 @@ public class MessageController {
|
||||
|
||||
Collection<AccountMismatchedDevices> accountMismatchedDevices = new ArrayList<>();
|
||||
Collection<AccountStaleDevices> accountStaleDevices = new ArrayList<>();
|
||||
uuidToAccountMap.values().forEach(account -> {
|
||||
accountsByServiceIdentifier.forEach((serviceIdentifier, account) -> {
|
||||
|
||||
if (isStory) {
|
||||
checkStoryRateLimit(account);
|
||||
@@ -434,10 +427,10 @@ public class MessageController {
|
||||
accountToDeviceIdAndRegistrationIdMap.get(account).stream(),
|
||||
false);
|
||||
} catch (MismatchedDevicesException e) {
|
||||
accountMismatchedDevices.add(new AccountMismatchedDevices(account.getUuid(),
|
||||
accountMismatchedDevices.add(new AccountMismatchedDevices(serviceIdentifier,
|
||||
new MismatchedDevices(e.getMissingDevices(), e.getExtraDevices())));
|
||||
} catch (StaleDevicesException e) {
|
||||
accountStaleDevices.add(new AccountStaleDevices(account.getUuid(), new StaleDevices(e.getStaleDevices())));
|
||||
accountStaleDevices.add(new AccountStaleDevices(serviceIdentifier, new StaleDevices(e.getStaleDevices())));
|
||||
}
|
||||
});
|
||||
if (!accountMismatchedDevices.isEmpty()) {
|
||||
@@ -455,7 +448,7 @@ public class MessageController {
|
||||
.build();
|
||||
}
|
||||
|
||||
List<UUID> uuids404 = Collections.synchronizedList(new ArrayList<>());
|
||||
List<ServiceIdentifier> uuids404 = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
try {
|
||||
final Counter sentMessageCounter = Metrics.counter(SENT_MESSAGE_COUNTER_NAME, Tags.of(
|
||||
@@ -463,18 +456,18 @@ public class MessageController {
|
||||
Tag.of(EPHEMERAL_TAG_NAME, String.valueOf(online)),
|
||||
Tag.of(SENDER_TYPE_TAG_NAME, SENDER_TYPE_UNIDENTIFIED)));
|
||||
|
||||
multiRecipientMessageExecutor.invokeAll(Arrays.stream(multiRecipientMessage.getRecipients())
|
||||
multiRecipientMessageExecutor.invokeAll(Arrays.stream(multiRecipientMessage.recipients())
|
||||
.map(recipient -> (Callable<Void>) () -> {
|
||||
Account destinationAccount = uuidToAccountMap.get(recipient.getUuid());
|
||||
Account destinationAccount = accountsByServiceIdentifier.get(recipient.uuid());
|
||||
|
||||
// we asserted this must exist in validateCompleteDeviceList
|
||||
Device destinationDevice = destinationAccount.getDevice(recipient.getDeviceId()).orElseThrow();
|
||||
Device destinationDevice = destinationAccount.getDevice(recipient.deviceId()).orElseThrow();
|
||||
sentMessageCounter.increment();
|
||||
try {
|
||||
sendCommonPayloadMessage(destinationAccount, destinationDevice, timestamp, online, isStory, isUrgent,
|
||||
recipient, multiRecipientMessage.getCommonPayload());
|
||||
recipient, multiRecipientMessage.commonPayload());
|
||||
} catch (NoSuchUserException e) {
|
||||
uuids404.add(destinationAccount.getUuid());
|
||||
uuids404.add(recipient.uuid());
|
||||
}
|
||||
return null;
|
||||
})
|
||||
@@ -486,7 +479,7 @@ public class MessageController {
|
||||
return Response.ok(new SendMultiRecipientMessageResponse(uuids404)).build();
|
||||
}
|
||||
|
||||
private void checkAccessKeys(CombinedUnidentifiedSenderAccessKeys accessKeys, Map<UUID, Account> uuidToAccountMap) {
|
||||
private void checkAccessKeys(final CombinedUnidentifiedSenderAccessKeys accessKeys, final Collection<Account> destinationAccounts) {
|
||||
// We should not have null access keys when checking access; bail out early.
|
||||
if (accessKeys == null) {
|
||||
throw new WebApplicationException(Status.UNAUTHORIZED);
|
||||
@@ -494,7 +487,7 @@ public class MessageController {
|
||||
AtomicBoolean throwUnauthorized = new AtomicBoolean(false);
|
||||
byte[] empty = new byte[16];
|
||||
final Optional<byte[]> UNRESTRICTED_UNIDENTIFIED_ACCESS_KEY = Optional.of(new byte[16]);
|
||||
byte[] combinedUnknownAccessKeys = uuidToAccountMap.values().stream()
|
||||
byte[] combinedUnknownAccessKeys = destinationAccounts.stream()
|
||||
.map(account -> {
|
||||
if (account.isUnrestrictedUnidentifiedAccess()) {
|
||||
return UNRESTRICTED_UNIDENTIFIED_ACCESS_KEY;
|
||||
@@ -595,8 +588,8 @@ public class MessageController {
|
||||
if (deletedMessage.hasSourceUuid() && deletedMessage.getType() != Type.SERVER_DELIVERY_RECEIPT) {
|
||||
try {
|
||||
receiptSender.sendReceipt(
|
||||
UUID.fromString(deletedMessage.getDestinationUuid()), auth.getAuthenticatedDevice().getId(),
|
||||
UUID.fromString(deletedMessage.getSourceUuid()), deletedMessage.getTimestamp());
|
||||
ServiceIdentifier.valueOf(deletedMessage.getDestinationUuid()), auth.getAuthenticatedDevice().getId(),
|
||||
AciServiceIdentifier.valueOf(deletedMessage.getSourceUuid()), deletedMessage.getTimestamp());
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to send delivery receipt", e);
|
||||
}
|
||||
@@ -663,7 +656,7 @@ public class MessageController {
|
||||
Optional<AuthenticatedAccount> source,
|
||||
Account destinationAccount,
|
||||
Device destinationDevice,
|
||||
UUID destinationUuid,
|
||||
ServiceIdentifier destinationIdentifier,
|
||||
long timestamp,
|
||||
boolean online,
|
||||
boolean story,
|
||||
@@ -679,7 +672,7 @@ public class MessageController {
|
||||
Account sourceAccount = source.map(AuthenticatedAccount::getAccount).orElse(null);
|
||||
Long sourceDeviceId = source.map(account -> account.getAuthenticatedDevice().getId()).orElse(null);
|
||||
envelope = incomingMessage.toEnvelope(
|
||||
destinationUuid,
|
||||
destinationIdentifier,
|
||||
sourceAccount,
|
||||
sourceDeviceId,
|
||||
timestamp == 0 ? System.currentTimeMillis() : timestamp,
|
||||
@@ -709,10 +702,10 @@ public class MessageController {
|
||||
try {
|
||||
Envelope.Builder messageBuilder = Envelope.newBuilder();
|
||||
long serverTimestamp = System.currentTimeMillis();
|
||||
byte[] recipientKeyMaterial = recipient.getPerRecipientKeyMaterial();
|
||||
byte[] recipientKeyMaterial = recipient.perRecipientKeyMaterial();
|
||||
|
||||
byte[] payload = new byte[1 + recipientKeyMaterial.length + commonPayload.length];
|
||||
payload[0] = MultiRecipientMessageProvider.VERSION;
|
||||
payload[0] = MultiRecipientMessageProvider.AMBIGUOUS_ID_VERSION_IDENTIFIER;
|
||||
System.arraycopy(recipientKeyMaterial, 0, payload, 1, recipientKeyMaterial.length);
|
||||
System.arraycopy(commonPayload, 0, payload, 1 + recipientKeyMaterial.length, commonPayload.length);
|
||||
|
||||
@@ -723,7 +716,7 @@ public class MessageController {
|
||||
.setContent(ByteString.copyFrom(payload))
|
||||
.setStory(story)
|
||||
.setUrgent(urgent)
|
||||
.setDestinationUuid(destinationAccount.getUuid().toString());
|
||||
.setDestinationUuid(new AciServiceIdentifier(destinationAccount.getUuid()).toServiceIdentifierString());
|
||||
|
||||
messageSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build(), online);
|
||||
} catch (NotPushRegisteredException e) {
|
||||
|
||||
@@ -90,6 +90,9 @@ import org.whispersystems.textsecuregcm.entities.ExpiringProfileKeyCredentialPro
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.UserCapabilities;
|
||||
import org.whispersystems.textsecuregcm.entities.VersionedProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
@@ -234,33 +237,33 @@ public class ProfileController {
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{uuid}/{version}")
|
||||
@Path("/{identifier}/{version}")
|
||||
public VersionedProfileResponse getProfile(
|
||||
@Auth Optional<AuthenticatedAccount> auth,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@PathParam("uuid") UUID uuid,
|
||||
@PathParam("identifier") AciServiceIdentifier accountIdentifier,
|
||||
@PathParam("version") String version)
|
||||
throws RateLimitExceededException {
|
||||
|
||||
final Optional<Account> maybeRequester = auth.map(AuthenticatedAccount::getAccount);
|
||||
final Account targetAccount = verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, uuid);
|
||||
final Account targetAccount = verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, accountIdentifier);
|
||||
|
||||
return buildVersionedProfileResponse(targetAccount,
|
||||
version,
|
||||
isSelfProfileRequest(maybeRequester, uuid),
|
||||
isSelfProfileRequest(maybeRequester, accountIdentifier),
|
||||
containerRequestContext);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Path("/{uuid}/{version}/{credentialRequest}")
|
||||
@Path("/{identifier}/{version}/{credentialRequest}")
|
||||
public CredentialProfileResponse getProfile(
|
||||
@Auth Optional<AuthenticatedAccount> auth,
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@PathParam("uuid") UUID uuid,
|
||||
@PathParam("identifier") AciServiceIdentifier accountIdentifier,
|
||||
@PathParam("version") String version,
|
||||
@PathParam("credentialRequest") String credentialRequest,
|
||||
@QueryParam("credentialType") String credentialType)
|
||||
@@ -271,8 +274,8 @@ public class ProfileController {
|
||||
}
|
||||
|
||||
final Optional<Account> maybeRequester = auth.map(AuthenticatedAccount::getAccount);
|
||||
final Account targetAccount = verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, uuid);
|
||||
final boolean isSelf = isSelfProfileRequest(maybeRequester, uuid);
|
||||
final Account targetAccount = verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, accountIdentifier);
|
||||
final boolean isSelf = isSelfProfileRequest(maybeRequester, accountIdentifier);
|
||||
|
||||
return buildExpiringProfileKeyCredentialProfileResponse(targetAccount,
|
||||
version,
|
||||
@@ -293,34 +296,38 @@ public class ProfileController {
|
||||
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
|
||||
@Context ContainerRequestContext containerRequestContext,
|
||||
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent,
|
||||
@PathParam("identifier") UUID identifier,
|
||||
@PathParam("identifier") ServiceIdentifier identifier,
|
||||
@QueryParam("ca") boolean useCaCertificate)
|
||||
throws RateLimitExceededException {
|
||||
|
||||
final Optional<Account> maybeAccountByPni = accountsManager.getByPhoneNumberIdentifier(identifier);
|
||||
final Optional<Account> maybeRequester = auth.map(AuthenticatedAccount::getAccount);
|
||||
|
||||
final BaseProfileResponse profileResponse;
|
||||
return switch (identifier.identityType()) {
|
||||
case ACI -> {
|
||||
final AciServiceIdentifier aciServiceIdentifier = (AciServiceIdentifier) identifier;
|
||||
|
||||
if (maybeAccountByPni.isPresent()) {
|
||||
if (maybeRequester.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
} else {
|
||||
rateLimiters.getProfileLimiter().validate(maybeRequester.get().getUuid());
|
||||
final Account targetAccount =
|
||||
verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, aciServiceIdentifier);
|
||||
|
||||
yield buildBaseProfileResponseForAccountIdentity(targetAccount,
|
||||
isSelfProfileRequest(maybeRequester, aciServiceIdentifier),
|
||||
containerRequestContext);
|
||||
}
|
||||
case PNI -> {
|
||||
final Optional<Account> maybeAccountByPni = accountsManager.getByPhoneNumberIdentifier(identifier.uuid());
|
||||
|
||||
OptionalAccess.verify(maybeRequester, Optional.empty(), maybeAccountByPni);
|
||||
if (maybeRequester.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
} else {
|
||||
rateLimiters.getProfileLimiter().validate(maybeRequester.get().getUuid());
|
||||
}
|
||||
|
||||
profileResponse = buildBaseProfileResponseForPhoneNumberIdentity(maybeAccountByPni.get());
|
||||
} else {
|
||||
final Account targetAccount = verifyPermissionToReceiveAccountIdentityProfile(maybeRequester, accessKey, identifier);
|
||||
OptionalAccess.verify(maybeRequester, Optional.empty(), maybeAccountByPni);
|
||||
|
||||
profileResponse = buildBaseProfileResponseForAccountIdentity(targetAccount,
|
||||
isSelfProfileRequest(maybeRequester, identifier),
|
||||
containerRequestContext);
|
||||
}
|
||||
|
||||
return profileResponse;
|
||||
assert maybeAccountByPni.isPresent();
|
||||
yield buildBaseProfileResponseForPhoneNumberIdentity(maybeAccountByPni.get());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Timed
|
||||
@@ -363,35 +370,24 @@ public class ProfileController {
|
||||
private void checkFingerprintAndAdd(BatchIdentityCheckRequest.Element element,
|
||||
Collection<BatchIdentityCheckResponse.Element> responseElements, MessageDigest md) {
|
||||
|
||||
final Optional<Account> maybeAccount;
|
||||
final boolean usePhoneNumberIdentity;
|
||||
if (element.aci() != null) {
|
||||
maybeAccount = accountsManager.getByAccountIdentifier(element.aci());
|
||||
usePhoneNumberIdentity = false;
|
||||
} else {
|
||||
final Optional<Account> maybeAciAccount = accountsManager.getByAccountIdentifier(element.uuid());
|
||||
|
||||
if (maybeAciAccount.isEmpty()) {
|
||||
maybeAccount = accountsManager.getByPhoneNumberIdentifier(element.uuid());
|
||||
usePhoneNumberIdentity = true;
|
||||
} else {
|
||||
maybeAccount = maybeAciAccount;
|
||||
usePhoneNumberIdentity = false;
|
||||
}
|
||||
}
|
||||
final Optional<Account> maybeAccount = accountsManager.getByServiceIdentifier(element.uuid());
|
||||
|
||||
maybeAccount.ifPresent(account -> {
|
||||
if (account.getIdentityKey() == null || account.getPhoneNumberIdentityKey() == null) {
|
||||
return;
|
||||
}
|
||||
final IdentityKey identityKey =
|
||||
usePhoneNumberIdentity ? account.getPhoneNumberIdentityKey() : account.getIdentityKey();
|
||||
|
||||
final IdentityKey identityKey = switch (element.uuid().identityType()) {
|
||||
case ACI -> account.getIdentityKey();
|
||||
case PNI -> account.getPhoneNumberIdentityKey();
|
||||
};
|
||||
|
||||
md.reset();
|
||||
byte[] digest = md.digest(identityKey.serialize());
|
||||
byte[] fingerprint = Util.truncate(digest, 4);
|
||||
|
||||
if (!Arrays.equals(fingerprint, element.fingerprint())) {
|
||||
responseElements.add(new BatchIdentityCheckResponse.Element(element.aci(), element.uuid(), identityKey));
|
||||
responseElements.add(new BatchIdentityCheckResponse.Element(element.uuid(), identityKey));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -454,7 +450,7 @@ public class ProfileController {
|
||||
getAcceptableLanguagesForRequest(containerRequestContext),
|
||||
account.getBadges(),
|
||||
isSelf),
|
||||
account.getUuid());
|
||||
new AciServiceIdentifier(account.getUuid()));
|
||||
}
|
||||
|
||||
private BaseProfileResponse buildBaseProfileResponseForPhoneNumberIdentity(final Account account) {
|
||||
@@ -463,7 +459,7 @@ public class ProfileController {
|
||||
false,
|
||||
UserCapabilities.createForAccount(account),
|
||||
Collections.emptyList(),
|
||||
account.getPhoneNumberIdentifier());
|
||||
new PniServiceIdentifier(account.getPhoneNumberIdentifier()));
|
||||
}
|
||||
|
||||
private ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse(
|
||||
@@ -562,7 +558,7 @@ public class ProfileController {
|
||||
*
|
||||
* @param maybeRequester the authenticated account requesting the profile, if any
|
||||
* @param maybeAccessKey an anonymous access key for the target account
|
||||
* @param targetUuid the ACI of the target account
|
||||
* @param accountIdentifier the ACI of the target account
|
||||
*
|
||||
* @return the target account
|
||||
*
|
||||
@@ -573,7 +569,7 @@ public class ProfileController {
|
||||
*/
|
||||
private Account verifyPermissionToReceiveAccountIdentityProfile(final Optional<Account> maybeRequester,
|
||||
final Optional<Anonymous> maybeAccessKey,
|
||||
final UUID targetUuid) throws RateLimitExceededException {
|
||||
final AciServiceIdentifier accountIdentifier) throws RateLimitExceededException {
|
||||
|
||||
if (maybeRequester.isEmpty() && maybeAccessKey.isEmpty()) {
|
||||
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
|
||||
@@ -583,7 +579,7 @@ public class ProfileController {
|
||||
rateLimiters.getProfileLimiter().validate(maybeRequester.get().getUuid());
|
||||
}
|
||||
|
||||
final Optional<Account> maybeTargetAccount = accountsManager.getByAccountIdentifier(targetUuid);
|
||||
final Optional<Account> maybeTargetAccount = accountsManager.getByAccountIdentifier(accountIdentifier.uuid());
|
||||
|
||||
OptionalAccess.verify(maybeRequester, maybeAccessKey, maybeTargetAccount);
|
||||
assert maybeTargetAccount.isPresent();
|
||||
@@ -591,7 +587,7 @@ public class ProfileController {
|
||||
return maybeTargetAccount.get();
|
||||
}
|
||||
|
||||
private boolean isSelfProfileRequest(final Optional<Account> maybeRequester, final UUID targetUuid) {
|
||||
return maybeRequester.map(requester -> requester.getUuid().equals(targetUuid)).orElse(false);
|
||||
private boolean isSelfProfileRequest(final Optional<Account> maybeRequester, final AciServiceIdentifier targetIdentifier) {
|
||||
return maybeRequester.map(requester -> requester.getUuid().equals(targetIdentifier.uuid())).orElse(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.UUID;
|
||||
|
||||
public record AccountIdentifierResponse(@NotNull UUID uuid) {}
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter;
|
||||
|
||||
public record AccountIdentifierResponse(@NotNull
|
||||
@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(using = ServiceIdentifierAdapter.AciServiceIdentifierDeserializer.class)
|
||||
AciServiceIdentifier uuid) {}
|
||||
|
||||
@@ -5,22 +5,14 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.UUID;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter;
|
||||
|
||||
public class AccountMismatchedDevices {
|
||||
@JsonProperty
|
||||
public final UUID uuid;
|
||||
public record AccountMismatchedDevices(@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class)
|
||||
ServiceIdentifier uuid,
|
||||
|
||||
@JsonProperty
|
||||
public final MismatchedDevices devices;
|
||||
|
||||
public String toString() {
|
||||
return "AccountMismatchedDevices(" + uuid + ", " + devices + ")";
|
||||
}
|
||||
|
||||
public AccountMismatchedDevices(final UUID uuid, final MismatchedDevices devices) {
|
||||
this.uuid = uuid;
|
||||
this.devices = devices;
|
||||
}
|
||||
MismatchedDevices devices) {
|
||||
}
|
||||
|
||||
@@ -5,22 +5,14 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import java.util.UUID;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter;
|
||||
|
||||
public class AccountStaleDevices {
|
||||
@JsonProperty
|
||||
public final UUID uuid;
|
||||
public record AccountStaleDevices(@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class)
|
||||
ServiceIdentifier uuid,
|
||||
|
||||
@JsonProperty
|
||||
public final StaleDevices devices;
|
||||
|
||||
public String toString() {
|
||||
return "AccountStaleDevices(" + uuid + ", " + devices + ")";
|
||||
}
|
||||
|
||||
public AccountStaleDevices(final UUID uuid, final StaleDevices devices) {
|
||||
this.uuid = uuid;
|
||||
this.devices = devices;
|
||||
}
|
||||
StaleDevices devices) {
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class BaseProfileResponse {
|
||||
|
||||
@@ -35,7 +35,9 @@ public class BaseProfileResponse {
|
||||
private List<Badge> badges;
|
||||
|
||||
@JsonProperty
|
||||
private UUID uuid;
|
||||
@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class)
|
||||
private ServiceIdentifier uuid;
|
||||
|
||||
public BaseProfileResponse() {
|
||||
}
|
||||
@@ -45,7 +47,7 @@ public class BaseProfileResponse {
|
||||
final boolean unrestrictedUnidentifiedAccess,
|
||||
final UserCapabilities capabilities,
|
||||
final List<Badge> badges,
|
||||
final UUID uuid) {
|
||||
final ServiceIdentifier uuid) {
|
||||
|
||||
this.identityKey = identityKey;
|
||||
this.unidentifiedAccess = unidentifiedAccess;
|
||||
@@ -75,7 +77,7 @@ public class BaseProfileResponse {
|
||||
return badges;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
public ServiceIdentifier getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.util.ExactlySize;
|
||||
import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter;
|
||||
|
||||
public record BatchIdentityCheckRequest(@Valid @NotNull @Size(max = 1000) List<Element> elements) {
|
||||
|
||||
@@ -20,18 +22,13 @@ public record BatchIdentityCheckRequest(@Valid @NotNull @Size(max = 1000) List<E
|
||||
* @param fingerprint most significant 4 bytes of SHA-256 of the 33-byte identity key field (32-byte curve25519 public
|
||||
* key prefixed with 0x05)
|
||||
*/
|
||||
public record Element(@Deprecated @Nullable UUID aci,
|
||||
@Nullable UUID uuid,
|
||||
@NotNull @ExactlySize(4) byte[] fingerprint) {
|
||||
public record Element(@NotNull
|
||||
@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class)
|
||||
ServiceIdentifier uuid,
|
||||
|
||||
public Element {
|
||||
if (aci == null && uuid == null) {
|
||||
throw new IllegalArgumentException("aci and uuid cannot both be null");
|
||||
}
|
||||
|
||||
if (aci != null && uuid != null) {
|
||||
throw new IllegalArgumentException("aci and uuid cannot both be non-null");
|
||||
}
|
||||
}
|
||||
@NotNull
|
||||
@ExactlySize(4)
|
||||
byte[] fingerprint) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,39 +6,27 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import java.util.List;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter;
|
||||
|
||||
public record BatchIdentityCheckResponse(@Valid List<Element> elements) {
|
||||
|
||||
public record Element(@Deprecated
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@Nullable UUID aci,
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@Nullable UUID uuid,
|
||||
public record Element(@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class)
|
||||
@NotNull
|
||||
ServiceIdentifier uuid,
|
||||
|
||||
@NotNull
|
||||
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
|
||||
IdentityKey identityKey) {
|
||||
|
||||
public Element {
|
||||
if (aci == null && uuid == null) {
|
||||
throw new IllegalArgumentException("aci and uuid cannot both be null");
|
||||
}
|
||||
|
||||
if (aci != null && uuid != null) {
|
||||
throw new IllegalArgumentException("aci and uuid cannot both be non-null");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,15 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
|
||||
public record IncomingMessage(int type, long destinationDeviceId, int destinationRegistrationId, String content) {
|
||||
|
||||
public MessageProtos.Envelope toEnvelope(final UUID destinationUuid,
|
||||
public MessageProtos.Envelope toEnvelope(final ServiceIdentifier destinationIdentifier,
|
||||
@Nullable Account sourceAccount,
|
||||
@Nullable Long sourceDeviceId,
|
||||
final long timestamp,
|
||||
@@ -32,13 +33,13 @@ public record IncomingMessage(int type, long destinationDeviceId, int destinatio
|
||||
envelopeBuilder.setType(envelopeType)
|
||||
.setTimestamp(timestamp)
|
||||
.setServerTimestamp(System.currentTimeMillis())
|
||||
.setDestinationUuid(destinationUuid.toString())
|
||||
.setDestinationUuid(destinationIdentifier.toServiceIdentifierString())
|
||||
.setStory(story)
|
||||
.setUrgent(urgent);
|
||||
|
||||
if (sourceAccount != null && sourceDeviceId != null) {
|
||||
envelopeBuilder
|
||||
.setSourceUuid(sourceAccount.getUuid().toString())
|
||||
.setSourceUuid(new AciServiceIdentifier(sourceAccount.getUuid()).toServiceIdentifierString())
|
||||
.setSourceDevice(sourceDeviceId.intValue());
|
||||
}
|
||||
|
||||
|
||||
@@ -6,31 +6,15 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MismatchedDevices {
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description = "Devices present on the account but absent in the request")
|
||||
public List<Long> missingDevices;
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description = "Devices absent on the request but present in the account")
|
||||
public List<Long> extraDevices;
|
||||
|
||||
@VisibleForTesting
|
||||
public MismatchedDevices() {}
|
||||
|
||||
public String toString() {
|
||||
return "MismatchedDevices(" + missingDevices + ", " + extraDevices + ")";
|
||||
}
|
||||
|
||||
public MismatchedDevices(List<Long> missingDevices, List<Long> extraDevices) {
|
||||
this.missingDevices = missingDevices;
|
||||
this.extraDevices = extraDevices;
|
||||
}
|
||||
public record MismatchedDevices(@JsonProperty
|
||||
@Schema(description = "Devices present on the account but absent in the request")
|
||||
List<Long> missingDevices,
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description = "Devices absent on the request but present in the account")
|
||||
List<Long> extraDevices) {
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
import static com.codahale.metrics.MetricRegistry.name;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.Objects;
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.Max;
|
||||
@@ -16,58 +16,33 @@ import javax.validation.constraints.Min;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.providers.MultiRecipientMessageProvider;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter;
|
||||
|
||||
public class MultiRecipientMessage {
|
||||
public record MultiRecipientMessage(
|
||||
@NotNull @Size(min = 1, max = MultiRecipientMessageProvider.MAX_RECIPIENT_COUNT) @Valid Recipient[] recipients,
|
||||
@NotNull @Size(min = 32) byte[] commonPayload) {
|
||||
|
||||
private static final Counter REJECT_DUPLICATE_RECIPIENT_COUNTER =
|
||||
Metrics.counter(
|
||||
name(MessageController.class, "rejectDuplicateRecipients"),
|
||||
"multiRecipient", "false");
|
||||
|
||||
public static class Recipient {
|
||||
|
||||
@NotNull
|
||||
private final UUID uuid;
|
||||
|
||||
@Min(1)
|
||||
private final long deviceId;
|
||||
|
||||
@Min(0)
|
||||
@Max(65535)
|
||||
private final int registrationId;
|
||||
|
||||
@Size(min = 48, max = 48)
|
||||
@NotNull
|
||||
private final byte[] perRecipientKeyMaterial;
|
||||
|
||||
public Recipient(UUID uuid, long deviceId, int registrationId, byte[] perRecipientKeyMaterial) {
|
||||
this.uuid = uuid;
|
||||
this.deviceId = deviceId;
|
||||
this.registrationId = registrationId;
|
||||
this.perRecipientKeyMaterial = perRecipientKeyMaterial;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public long getDeviceId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public byte[] getPerRecipientKeyMaterial() {
|
||||
return perRecipientKeyMaterial;
|
||||
}
|
||||
public record Recipient(@NotNull
|
||||
@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class)
|
||||
ServiceIdentifier uuid,
|
||||
@Min(1) long deviceId,
|
||||
@Min(0) @Max(65535) int registrationId,
|
||||
@Size(min = 48, max = 48) @NotNull byte[] perRecipientKeyMaterial) {
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
@@ -75,60 +50,48 @@ public class MultiRecipientMessage {
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
|
||||
Recipient recipient = (Recipient) o;
|
||||
|
||||
if (deviceId != recipient.deviceId)
|
||||
return false;
|
||||
if (registrationId != recipient.registrationId)
|
||||
return false;
|
||||
if (!uuid.equals(recipient.uuid))
|
||||
return false;
|
||||
return Arrays.equals(perRecipientKeyMaterial, recipient.perRecipientKeyMaterial);
|
||||
return deviceId == recipient.deviceId && registrationId == recipient.registrationId && uuid.equals(recipient.uuid)
|
||||
&& Arrays.equals(perRecipientKeyMaterial, recipient.perRecipientKeyMaterial);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = uuid.hashCode();
|
||||
result = 31 * result + (int) (deviceId ^ (deviceId >>> 32));
|
||||
result = 31 * result + registrationId;
|
||||
int result = Objects.hash(uuid, deviceId, registrationId);
|
||||
result = 31 * result + Arrays.hashCode(perRecipientKeyMaterial);
|
||||
return result;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Recipient(" + uuid + ", " + deviceId + ", " + registrationId + ", " + Arrays.toString(perRecipientKeyMaterial) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Size(min = 1, max = MultiRecipientMessageProvider.MAX_RECIPIENT_COUNT)
|
||||
@Valid
|
||||
private final Recipient[] recipients;
|
||||
|
||||
@NotNull
|
||||
@Size(min = 32)
|
||||
private final byte[] commonPayload;
|
||||
|
||||
public MultiRecipientMessage(Recipient[] recipients, byte[] commonPayload) {
|
||||
this.recipients = recipients;
|
||||
this.commonPayload = commonPayload;
|
||||
}
|
||||
|
||||
public Recipient[] getRecipients() {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
public byte[] getCommonPayload() {
|
||||
return commonPayload;
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
public boolean hasNoDuplicateRecipients() {
|
||||
boolean valid = Arrays.stream(recipients).map(r -> new Pair<>(r.getUuid(), r.getDeviceId())).distinct().count() == recipients.length;
|
||||
boolean valid =
|
||||
Arrays.stream(recipients).map(r -> new Pair<>(r.uuid(), r.deviceId())).distinct().count() == recipients.length;
|
||||
if (!valid) {
|
||||
REJECT_DUPLICATE_RECIPIENT_COUNTER.increment();
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
MultiRecipientMessage that = (MultiRecipientMessage) o;
|
||||
return Arrays.equals(recipients, that.recipients) && Arrays.equals(commonPayload, that.commonPayload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Arrays.hashCode(recipients);
|
||||
result = 31 * result + Arrays.hashCode(commonPayload);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,28 +5,50 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.protobuf.ByteString;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter;
|
||||
|
||||
public record OutgoingMessageEntity(UUID guid, int type, long timestamp, @Nullable UUID sourceUuid, int sourceDevice,
|
||||
UUID destinationUuid, @Nullable UUID updatedPni, byte[] content,
|
||||
long serverTimestamp, boolean urgent, boolean story, @Nullable byte[] reportSpamToken) {
|
||||
public record OutgoingMessageEntity(UUID guid,
|
||||
int type,
|
||||
long timestamp,
|
||||
|
||||
@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class)
|
||||
@Nullable
|
||||
ServiceIdentifier sourceUuid,
|
||||
|
||||
int sourceDevice,
|
||||
|
||||
@JsonSerialize(using = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(using = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class)
|
||||
ServiceIdentifier destinationUuid,
|
||||
|
||||
@Nullable UUID updatedPni,
|
||||
byte[] content,
|
||||
long serverTimestamp,
|
||||
boolean urgent,
|
||||
boolean story,
|
||||
@Nullable byte[] reportSpamToken) {
|
||||
|
||||
public MessageProtos.Envelope toEnvelope() {
|
||||
final MessageProtos.Envelope.Builder builder = MessageProtos.Envelope.newBuilder()
|
||||
.setType(MessageProtos.Envelope.Type.forNumber(type()))
|
||||
.setTimestamp(timestamp())
|
||||
.setServerTimestamp(serverTimestamp())
|
||||
.setDestinationUuid(destinationUuid().toString())
|
||||
.setDestinationUuid(destinationUuid().toServiceIdentifierString())
|
||||
.setServerGuid(guid().toString())
|
||||
.setStory(story)
|
||||
.setUrgent(urgent);
|
||||
|
||||
if (sourceUuid() != null) {
|
||||
builder.setSourceUuid(sourceUuid().toString());
|
||||
builder.setSourceUuid(sourceUuid().toServiceIdentifierString());
|
||||
builder.setSourceDevice(sourceDevice());
|
||||
}
|
||||
|
||||
@@ -51,9 +73,9 @@ public record OutgoingMessageEntity(UUID guid, int type, long timestamp, @Nullab
|
||||
UUID.fromString(envelope.getServerGuid()),
|
||||
envelope.getType().getNumber(),
|
||||
envelope.getTimestamp(),
|
||||
envelope.hasSourceUuid() ? UUID.fromString(envelope.getSourceUuid()) : null,
|
||||
envelope.hasSourceUuid() ? ServiceIdentifier.valueOf(envelope.getSourceUuid()) : null,
|
||||
envelope.getSourceDevice(),
|
||||
envelope.hasDestinationUuid() ? UUID.fromString(envelope.getDestinationUuid()) : null,
|
||||
envelope.hasDestinationUuid() ? ServiceIdentifier.valueOf(envelope.getDestinationUuid()) : null,
|
||||
envelope.hasUpdatedPni() ? UUID.fromString(envelope.getUpdatedPni()) : null,
|
||||
envelope.getContent().toByteArray(),
|
||||
envelope.getServerTimestamp(),
|
||||
|
||||
@@ -6,27 +6,15 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.util.ServiceIdentifierAdapter;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SendMultiRecipientMessageResponse {
|
||||
@JsonProperty
|
||||
private List<UUID> uuids404;
|
||||
|
||||
public SendMultiRecipientMessageResponse() {
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "SendMultiRecipientMessageResponse(" + uuids404 + ")";
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public List<UUID> getUUIDs404() {
|
||||
return this.uuids404;
|
||||
}
|
||||
|
||||
public SendMultiRecipientMessageResponse(final List<UUID> uuids404) {
|
||||
this.uuids404 = uuids404;
|
||||
}
|
||||
public record SendMultiRecipientMessageResponse(@JsonSerialize(contentUsing = ServiceIdentifierAdapter.ServiceIdentifierSerializer.class)
|
||||
@JsonDeserialize(contentUsing = ServiceIdentifierAdapter.ServiceIdentifierDeserializer.class)
|
||||
List<ServiceIdentifier> uuids404) {
|
||||
}
|
||||
|
||||
@@ -10,20 +10,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class StaleDevices {
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description = "Devices that are no longer active")
|
||||
private List<Long> staleDevices;
|
||||
|
||||
public StaleDevices() {}
|
||||
|
||||
public String toString() {
|
||||
return "StaleDevices(" + staleDevices + ")";
|
||||
}
|
||||
|
||||
public StaleDevices(List<Long> staleDevices) {
|
||||
this.staleDevices = staleDevices;
|
||||
}
|
||||
|
||||
public record StaleDevices(@JsonProperty
|
||||
@Schema(description = "Devices that are no longer active")
|
||||
List<Long> staleDevices) {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.identity;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
import java.util.UUID;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
|
||||
/**
|
||||
* An identifier for an account based on the account's ACI.
|
||||
*
|
||||
* @param uuid the account's ACI UUID
|
||||
*/
|
||||
@Schema(
|
||||
type = "string",
|
||||
description = "An identifier for an account based on the account's ACI"
|
||||
)
|
||||
public record AciServiceIdentifier(UUID uuid) implements ServiceIdentifier {
|
||||
|
||||
private static final IdentityType IDENTITY_TYPE = IdentityType.ACI;
|
||||
|
||||
@Override
|
||||
public IdentityType identityType() {
|
||||
return IDENTITY_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toServiceIdentifierString() {
|
||||
return uuid.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toCompactByteArray() {
|
||||
return UUIDUtil.toBytes(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toFixedWidthByteArray() {
|
||||
final ByteBuffer byteBuffer = ByteBuffer.allocate(17);
|
||||
byteBuffer.put(IDENTITY_TYPE.getBytePrefix());
|
||||
byteBuffer.putLong(uuid.getMostSignificantBits());
|
||||
byteBuffer.putLong(uuid.getLeastSignificantBits());
|
||||
byteBuffer.flip();
|
||||
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
public static AciServiceIdentifier valueOf(final String string) {
|
||||
return new AciServiceIdentifier(
|
||||
UUID.fromString(string.startsWith(IDENTITY_TYPE.getStringPrefix())
|
||||
? string.substring(IDENTITY_TYPE.getStringPrefix().length()) : string));
|
||||
}
|
||||
|
||||
public static AciServiceIdentifier fromBytes(final byte[] bytes) {
|
||||
final UUID uuid;
|
||||
|
||||
if (bytes.length == 17) {
|
||||
if (bytes[0] != IDENTITY_TYPE.getBytePrefix()) {
|
||||
throw new IllegalArgumentException("Unexpected byte array prefix: " + HexFormat.of().formatHex(new byte[] { bytes[0] }));
|
||||
}
|
||||
|
||||
uuid = UUIDUtil.fromBytes(Arrays.copyOfRange(bytes, 1, bytes.length));
|
||||
} else {
|
||||
uuid = UUIDUtil.fromBytes(bytes);
|
||||
}
|
||||
|
||||
return new AciServiceIdentifier(uuid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.identity;
|
||||
|
||||
public enum IdentityType {
|
||||
ACI((byte) 0x00, "ACI:"),
|
||||
PNI((byte) 0x01, "PNI:");
|
||||
|
||||
private final byte bytePrefix;
|
||||
private final String stringPrefix;
|
||||
|
||||
IdentityType(final byte bytePrefix, final String stringPrefix) {
|
||||
this.bytePrefix = bytePrefix;
|
||||
this.stringPrefix = stringPrefix;
|
||||
}
|
||||
|
||||
byte getBytePrefix() {
|
||||
return bytePrefix;
|
||||
}
|
||||
|
||||
String getStringPrefix() {
|
||||
return stringPrefix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.identity;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.whispersystems.textsecuregcm.util.UUIDUtil;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* An identifier for an account based on the account's phone number identifier (PNI).
|
||||
*
|
||||
* @param uuid the account's PNI UUID
|
||||
*/
|
||||
@Schema(
|
||||
type = "string",
|
||||
description = "An identifier for an account based on the account's phone number identifier (PNI)"
|
||||
)
|
||||
public record PniServiceIdentifier(UUID uuid) implements ServiceIdentifier {
|
||||
|
||||
private static final IdentityType IDENTITY_TYPE = IdentityType.PNI;
|
||||
|
||||
@Override
|
||||
public IdentityType identityType() {
|
||||
return IDENTITY_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toServiceIdentifierString() {
|
||||
return IDENTITY_TYPE.getStringPrefix() + uuid.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toCompactByteArray() {
|
||||
return toFixedWidthByteArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toFixedWidthByteArray() {
|
||||
final ByteBuffer byteBuffer = ByteBuffer.allocate(17);
|
||||
byteBuffer.put(IDENTITY_TYPE.getBytePrefix());
|
||||
byteBuffer.putLong(uuid.getMostSignificantBits());
|
||||
byteBuffer.putLong(uuid.getLeastSignificantBits());
|
||||
byteBuffer.flip();
|
||||
|
||||
return byteBuffer.array();
|
||||
}
|
||||
|
||||
public static PniServiceIdentifier valueOf(final String string) {
|
||||
if (!string.startsWith(IDENTITY_TYPE.getStringPrefix())) {
|
||||
throw new IllegalArgumentException("PNI account identifier did not start with \"PNI:\" prefix");
|
||||
}
|
||||
|
||||
return new PniServiceIdentifier(UUID.fromString(string.substring(IDENTITY_TYPE.getStringPrefix().length())));
|
||||
}
|
||||
|
||||
public static PniServiceIdentifier fromBytes(final byte[] bytes) {
|
||||
if (bytes.length == 17) {
|
||||
if (bytes[0] != IDENTITY_TYPE.getBytePrefix()) {
|
||||
throw new IllegalArgumentException("Unexpected byte array prefix: " + HexFormat.of().formatHex(new byte[] { bytes[0] }));
|
||||
}
|
||||
|
||||
return new PniServiceIdentifier(UUIDUtil.fromBytes(Arrays.copyOfRange(bytes, 1, bytes.length)));
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unexpected byte array length: " + bytes.length);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.identity;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A "service identifier" is a tuple of a UUID and identity type that identifies an account and identity within the
|
||||
* Signal service.
|
||||
*/
|
||||
@Schema(
|
||||
type = "string",
|
||||
description = "A service identifier is a tuple of a UUID and identity type that identifies an account and identity within the Signal service.",
|
||||
subTypes = {AciServiceIdentifier.class, PniServiceIdentifier.class}
|
||||
)
|
||||
public interface ServiceIdentifier {
|
||||
|
||||
/**
|
||||
* Returns the identity type of this account identifier.
|
||||
*
|
||||
* @return the identity type of this account identifier
|
||||
*/
|
||||
IdentityType identityType();
|
||||
|
||||
/**
|
||||
* Returns the UUID for this account identifier.
|
||||
*
|
||||
* @return the UUID for this account identifier
|
||||
*/
|
||||
UUID uuid();
|
||||
|
||||
/**
|
||||
* Returns a string representation of this account identifier in a format that clients can unambiguously resolve into
|
||||
* an identity type and UUID.
|
||||
*
|
||||
* @return a "strongly-typed" string representation of this account identifier
|
||||
*/
|
||||
String toServiceIdentifierString();
|
||||
|
||||
/**
|
||||
* Returns a compact binary representation of this account identifier.
|
||||
*
|
||||
* @return a binary representation of this account identifier
|
||||
*/
|
||||
byte[] toCompactByteArray();
|
||||
|
||||
/**
|
||||
* Returns a fixed-width binary representation of this account identifier.
|
||||
*
|
||||
* @return a binary representation of this account identifier
|
||||
*/
|
||||
byte[] toFixedWidthByteArray();
|
||||
|
||||
static ServiceIdentifier valueOf(final String string) {
|
||||
try {
|
||||
return AciServiceIdentifier.valueOf(string);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
return PniServiceIdentifier.valueOf(string);
|
||||
}
|
||||
}
|
||||
|
||||
static ServiceIdentifier fromBytes(final byte[] bytes) {
|
||||
try {
|
||||
return AciServiceIdentifier.fromBytes(bytes);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
return PniServiceIdentifier.fromBytes(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,22 +9,20 @@ import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import io.micrometer.core.instrument.Tag;
|
||||
import io.micrometer.core.instrument.Tags;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos;
|
||||
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||
|
||||
public final class MessageMetrics {
|
||||
|
||||
@@ -44,16 +42,15 @@ public final class MessageMetrics {
|
||||
final MessageProtos.Envelope envelope) {
|
||||
if (envelope.hasDestinationUuid()) {
|
||||
try {
|
||||
final UUID destinationUuid = UUID.fromString(envelope.getDestinationUuid());
|
||||
measureAccountDestinationUuidMismatches(account, destinationUuid);
|
||||
measureAccountDestinationUuidMismatches(account, ServiceIdentifier.valueOf(envelope.getDestinationUuid()));
|
||||
} catch (final IllegalArgumentException ignored) {
|
||||
logger.warn("Envelope had invalid destination UUID: {}", envelope.getDestinationUuid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void measureAccountDestinationUuidMismatches(final Account account, final UUID destinationUuid) {
|
||||
if (!destinationUuid.equals(account.getUuid()) && !destinationUuid.equals(account.getPhoneNumberIdentifier())) {
|
||||
private static void measureAccountDestinationUuidMismatches(final Account account, final ServiceIdentifier destinationIdentifier) {
|
||||
if (!account.isIdentifiedBy(destinationIdentifier)) {
|
||||
// In all cases, this represents a mismatch between the account’s current PNI and its PNI when the message was
|
||||
// sent. This is an expected case, but if this metric changes significantly, it could indicate an issue to
|
||||
// investigate.
|
||||
|
||||
@@ -11,7 +11,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.UUID;
|
||||
import javax.ws.rs.BadRequestException;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
@@ -21,6 +20,7 @@ import javax.ws.rs.core.NoContentException;
|
||||
import javax.ws.rs.ext.MessageBodyReader;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
import org.whispersystems.textsecuregcm.entities.MultiRecipientMessage;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
|
||||
@Provider
|
||||
@Consumes(MultiRecipientMessageProvider.MEDIA_TYPE)
|
||||
@@ -29,7 +29,30 @@ public class MultiRecipientMessageProvider implements MessageBodyReader<MultiRec
|
||||
public static final String MEDIA_TYPE = "application/vnd.signal-messenger.mrm";
|
||||
public static final int MAX_RECIPIENT_COUNT = 5000;
|
||||
public static final int MAX_MESSAGE_SIZE = Math.toIntExact(32 + DataSizeUnit.KIBIBYTES.toBytes(256));
|
||||
public static final byte VERSION = 0x22;
|
||||
|
||||
public static final byte AMBIGUOUS_ID_VERSION_IDENTIFIER = 0x22;
|
||||
public static final byte EXPLICIT_ID_VERSION_IDENTIFIER = 0x23;
|
||||
|
||||
private enum Version {
|
||||
AMBIGUOUS_ID(AMBIGUOUS_ID_VERSION_IDENTIFIER),
|
||||
EXPLICIT_ID(EXPLICIT_ID_VERSION_IDENTIFIER);
|
||||
|
||||
private final byte identifier;
|
||||
|
||||
Version(final byte identifier) {
|
||||
this.identifier = identifier;
|
||||
}
|
||||
|
||||
static Version forVersionByte(final byte versionByte) {
|
||||
for (final Version version : values()) {
|
||||
if (version.identifier == versionByte) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unrecognized version byte: " + versionByte);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
|
||||
@@ -44,23 +67,29 @@ public class MultiRecipientMessageProvider implements MessageBodyReader<MultiRec
|
||||
if (versionByte == -1) {
|
||||
throw new NoContentException("Empty body not allowed");
|
||||
}
|
||||
if (versionByte != VERSION) {
|
||||
|
||||
final Version version;
|
||||
|
||||
try {
|
||||
version = Version.forVersionByte((byte) versionByte);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new BadRequestException("Unsupported version");
|
||||
}
|
||||
|
||||
long count = readVarint(entityStream);
|
||||
if (count > MAX_RECIPIENT_COUNT) {
|
||||
throw new BadRequestException("Maximum recipient count exceeded");
|
||||
}
|
||||
MultiRecipientMessage.Recipient[] recipients = new MultiRecipientMessage.Recipient[Math.toIntExact(count)];
|
||||
for (int i = 0; i < Math.toIntExact(count); i++) {
|
||||
UUID uuid = readUuid(entityStream);
|
||||
ServiceIdentifier identifier = readIdentifier(entityStream, version);
|
||||
long deviceId = readVarint(entityStream);
|
||||
int registrationId = readU16(entityStream);
|
||||
byte[] perRecipientKeyMaterial = entityStream.readNBytes(48);
|
||||
if (perRecipientKeyMaterial.length != 48) {
|
||||
throw new IOException("Failed to read expected number of key material bytes for a recipient");
|
||||
}
|
||||
recipients[i] = new MultiRecipientMessage.Recipient(uuid, deviceId, registrationId, perRecipientKeyMaterial);
|
||||
recipients[i] = new MultiRecipientMessage.Recipient(identifier, deviceId, registrationId, perRecipientKeyMaterial);
|
||||
}
|
||||
|
||||
// caller is responsible for checking that the entity stream is at EOF when we return; if there are more bytes than
|
||||
@@ -73,32 +102,15 @@ public class MultiRecipientMessageProvider implements MessageBodyReader<MultiRec
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a UUID in network byte order and converts to a UUID object.
|
||||
* Reads a service identifier from the given stream.
|
||||
*/
|
||||
private UUID readUuid(InputStream stream) throws IOException {
|
||||
byte[] buffer = new byte[8];
|
||||
private ServiceIdentifier readIdentifier(final InputStream stream, final Version version) throws IOException {
|
||||
final byte[] uuidBytes = switch (version) {
|
||||
case AMBIGUOUS_ID -> stream.readNBytes(16);
|
||||
case EXPLICIT_ID -> stream.readNBytes(17);
|
||||
};
|
||||
|
||||
int read = stream.readNBytes(buffer, 0, 8);
|
||||
if (read != 8) {
|
||||
throw new IOException("Insufficient bytes for UUID");
|
||||
}
|
||||
long msb = convertNetworkByteOrderToLong(buffer);
|
||||
|
||||
read = stream.readNBytes(buffer, 0, 8);
|
||||
if (read != 8) {
|
||||
throw new IOException("Insufficient bytes for UUID");
|
||||
}
|
||||
long lsb = convertNetworkByteOrderToLong(buffer);
|
||||
|
||||
return new UUID(msb, lsb);
|
||||
}
|
||||
|
||||
private long convertNetworkByteOrderToLong(byte[] buffer) {
|
||||
long result = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
result = (result << 8) | (buffer[i] & 0xFFL);
|
||||
}
|
||||
return result;
|
||||
return ServiceIdentifier.fromBytes(uuidBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,11 +9,12 @@ import com.codahale.metrics.InstrumentedExecutorService;
|
||||
import com.codahale.metrics.SharedMetricRegistries;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
|
||||
import org.whispersystems.textsecuregcm.storage.Device;
|
||||
@@ -40,20 +41,20 @@ public class ReceiptSender {
|
||||
;
|
||||
}
|
||||
|
||||
public void sendReceipt(UUID sourceUuid, long sourceDeviceId, UUID destinationUuid, long messageId) {
|
||||
if (sourceUuid.equals(destinationUuid)) {
|
||||
public void sendReceipt(ServiceIdentifier sourceIdentifier, long sourceDeviceId, AciServiceIdentifier destinationIdentifier, long messageId) {
|
||||
if (sourceIdentifier.equals(destinationIdentifier)) {
|
||||
return;
|
||||
}
|
||||
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
accountManager.getByAccountIdentifier(destinationUuid).ifPresentOrElse(
|
||||
accountManager.getByAccountIdentifier(destinationIdentifier.uuid()).ifPresentOrElse(
|
||||
destinationAccount -> {
|
||||
final Envelope.Builder message = Envelope.newBuilder()
|
||||
.setServerTimestamp(System.currentTimeMillis())
|
||||
.setSourceUuid(sourceUuid.toString())
|
||||
.setSourceUuid(sourceIdentifier.toServiceIdentifierString())
|
||||
.setSourceDevice((int) sourceDeviceId)
|
||||
.setDestinationUuid(destinationUuid.toString())
|
||||
.setDestinationUuid(destinationIdentifier.toServiceIdentifierString())
|
||||
.setTimestamp(messageId)
|
||||
.setType(Envelope.Type.SERVER_DELIVERY_RECEIPT)
|
||||
.setUrgent(false);
|
||||
@@ -68,7 +69,7 @@ public class ReceiptSender {
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> logger.info("No longer registered: {}", destinationUuid)
|
||||
() -> logger.info("No longer registered: {}", destinationIdentifier)
|
||||
);
|
||||
|
||||
} catch (final Exception e) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
|
||||
@@ -123,13 +124,17 @@ public class Account {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether this account's account identifier or phone number identifier matches the given UUID.
|
||||
* Tests whether this account's account identifier or phone number identifier (depending on the given service
|
||||
* identifier's identity type) matches the given service identifier.
|
||||
*
|
||||
* @param identifier the identifier to test
|
||||
* @param serviceIdentifier the identifier to test
|
||||
* @return {@code true} if this account's identifier or phone number identifier matches
|
||||
*/
|
||||
public boolean isIdentifiedBy(final UUID identifier) {
|
||||
return uuid.equals(identifier) || (phoneNumberIdentifier != null && phoneNumberIdentifier.equals(identifier));
|
||||
public boolean isIdentifiedBy(final ServiceIdentifier serviceIdentifier) {
|
||||
return switch (serviceIdentifier.identityType()) {
|
||||
case ACI -> serviceIdentifier.uuid().equals(uuid);
|
||||
case PNI -> serviceIdentifier.uuid().equals(phoneNumberIdentifier);
|
||||
};
|
||||
}
|
||||
|
||||
public String getNumber() {
|
||||
|
||||
@@ -52,6 +52,7 @@ import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
import org.whispersystems.textsecuregcm.redis.RedisOperation;
|
||||
@@ -803,6 +804,13 @@ public class AccountsManager {
|
||||
);
|
||||
}
|
||||
|
||||
public Optional<Account> getByServiceIdentifier(final ServiceIdentifier serviceIdentifier) {
|
||||
return switch (serviceIdentifier.identityType()) {
|
||||
case ACI -> getByAccountIdentifier(serviceIdentifier.uuid());
|
||||
case PNI -> getByPhoneNumberIdentifier(serviceIdentifier.uuid());
|
||||
};
|
||||
}
|
||||
|
||||
public Optional<Account> getByAccountIdentifier(final UUID uuid) {
|
||||
return checkRedisThenAccounts(
|
||||
getByUuidTimer,
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.util.DestinationDeviceValidator;
|
||||
@@ -136,9 +137,9 @@ public class ChangeNumberManager {
|
||||
.setType(Envelope.Type.forNumber(message.type()))
|
||||
.setTimestamp(serverTimestamp)
|
||||
.setServerTimestamp(serverTimestamp)
|
||||
.setDestinationUuid(sourceAndDestinationAccount.getUuid().toString())
|
||||
.setDestinationUuid(new AciServiceIdentifier(sourceAndDestinationAccount.getUuid()).toServiceIdentifierString())
|
||||
.setContent(ByteString.copyFrom(contents.get()))
|
||||
.setSourceUuid(sourceAndDestinationAccount.getUuid().toString())
|
||||
.setSourceUuid(new AciServiceIdentifier(sourceAndDestinationAccount.getUuid()).toServiceIdentifierString())
|
||||
.setSourceDevice((int) Device.MASTER_ID)
|
||||
.setUpdatedPni(sourceAndDestinationAccount.getPhoneNumberIdentifier().toString())
|
||||
.setUrgent(true)
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 java.io.IOException;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
||||
|
||||
public class ServiceIdentifierAdapter {
|
||||
|
||||
public static class ServiceIdentifierSerializer extends JsonSerializer<ServiceIdentifier> {
|
||||
|
||||
@Override
|
||||
public void serialize(final ServiceIdentifier identifier, final JsonGenerator jsonGenerator, final SerializerProvider serializers)
|
||||
throws IOException {
|
||||
|
||||
jsonGenerator.writeString(identifier.toServiceIdentifierString());
|
||||
}
|
||||
}
|
||||
|
||||
public static class AciServiceIdentifierDeserializer extends JsonDeserializer<AciServiceIdentifier> {
|
||||
|
||||
@Override
|
||||
public AciServiceIdentifier deserialize(final JsonParser parser, final DeserializationContext context)
|
||||
throws IOException {
|
||||
|
||||
return AciServiceIdentifier.valueOf(parser.getValueAsString());
|
||||
}
|
||||
}
|
||||
|
||||
public static class PniServiceIdentifierDeserializer extends JsonDeserializer<PniServiceIdentifier> {
|
||||
|
||||
@Override
|
||||
public PniServiceIdentifier deserialize(final JsonParser parser, final DeserializationContext context)
|
||||
throws IOException {
|
||||
|
||||
return PniServiceIdentifier.valueOf(parser.getValueAsString());
|
||||
}
|
||||
}
|
||||
|
||||
public static class ServiceIdentifierDeserializer extends JsonDeserializer<ServiceIdentifier> {
|
||||
|
||||
@Override
|
||||
public ServiceIdentifier deserialize(final JsonParser parser, final DeserializationContext context)
|
||||
throws IOException {
|
||||
|
||||
return ServiceIdentifier.valueOf(parser.getValueAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.metrics.MessageMetrics;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
@@ -265,8 +267,8 @@ public class WebSocketConnection implements MessageAvailabilityListener, Displac
|
||||
}
|
||||
|
||||
try {
|
||||
receiptSender.sendReceipt(UUID.fromString(message.getDestinationUuid()),
|
||||
auth.getAuthenticatedDevice().getId(), UUID.fromString(message.getSourceUuid()),
|
||||
receiptSender.sendReceipt(ServiceIdentifier.valueOf(message.getDestinationUuid()),
|
||||
auth.getAuthenticatedDevice().getId(), AciServiceIdentifier.valueOf(message.getSourceUuid()),
|
||||
message.getTimestamp());
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("Could not parse UUID: {}", message.getSourceUuid());
|
||||
|
||||
Reference in New Issue
Block a user