Introduce "service identifiers"

This commit is contained in:
Jon Chambers
2023-07-21 09:34:10 -04:00
committed by GitHub
parent 4a6c7152cf
commit abb32bd919
39 changed files with 1304 additions and 588 deletions

View File

@@ -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

View File

@@ -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);

View File

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

View File

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