Allow the "get profile" endpoint to include a PNI credential

This commit is contained in:
Jon Chambers 2021-11-17 17:28:10 -05:00 committed by Jon Chambers
parent 93a7c60a15
commit 68412b3901
3 changed files with 151 additions and 67 deletions

View File

@ -5,8 +5,12 @@
package org.whispersystems.textsecuregcm.controllers;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
import com.codahale.metrics.annotation.Timed;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import java.security.SecureRandom;
import java.time.Clock;
import java.time.Duration;
@ -24,7 +28,9 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import javax.validation.Valid;
import javax.validation.valueextraction.Unwrapping;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
@ -39,14 +45,13 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.PniCredentialResponse;
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
@ -80,8 +85,6 @@ import org.whispersystems.textsecuregcm.util.Pair;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/profile")
public class ProfileController {
@ -103,6 +106,9 @@ public class ProfileController {
private final S3Client s3client;
private final String bucket;
private static final String PROFILE_KEY_CREDENTIAL_TYPE = "profileKey";
private static final String PNI_CREDENTIAL_TYPE = "pni";
private static final String LEGACY_GET_PROFILE_COUNTER_NAME = name(ProfileController.class, "legacyGetProfileByPlatform");
public ProfileController(
@ -205,7 +211,7 @@ public class ProfileController {
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{uuid}/{version}")
public Optional<Profile> getProfile(
public Profile getProfile(
@Auth Optional<AuthenticatedAccount> auth,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@Context ContainerRequestContext containerRequestContext,
@ -214,35 +220,36 @@ public class ProfileController {
throws RateLimitExceededException {
return getVersionedProfile(auth.map(AuthenticatedAccount::getAccount), accessKey,
getAcceptableLanguagesForRequest(containerRequestContext), uuid,
version, Optional.empty());
version, Optional.empty(), Optional.empty());
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{uuid}/{version}/{credentialRequest}")
public Optional<Profile> getProfile(
public Profile getProfile(
@Auth Optional<AuthenticatedAccount> auth,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@Context ContainerRequestContext containerRequestContext,
@PathParam("uuid") UUID uuid,
@PathParam("version") String version,
@PathParam("credentialRequest") String credentialRequest)
@PathParam("credentialRequest") String credentialRequest,
@QueryParam("credentialType") @DefaultValue(PROFILE_KEY_CREDENTIAL_TYPE) String credentialType)
throws RateLimitExceededException {
return getVersionedProfile(auth.map(AuthenticatedAccount::getAccount), accessKey,
getAcceptableLanguagesForRequest(containerRequestContext), uuid,
version, Optional.of(credentialRequest));
version, Optional.of(credentialRequest), Optional.of(credentialType));
}
private Optional<Profile> getVersionedProfile(
private Profile getVersionedProfile(
Optional<Account> requestAccount,
Optional<Anonymous> accessKey,
List<Locale> acceptableLanguages,
UUID uuid,
String version,
Optional<String> credentialRequest)
Optional<String> credentialRequest,
Optional<String> credentialType)
throws RateLimitExceededException {
try {
if (requestAccount.isEmpty() && accessKey.isEmpty()) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
@ -275,9 +282,31 @@ public class ProfileController {
.map(VersionedProfile::getPaymentAddress)
.orElse(null);
Optional<ProfileKeyCredentialResponse> credential = getProfileCredential(credentialRequest, profile, uuid);
final ProfileKeyCredentialResponse profileKeyCredentialResponse;
final PniCredentialResponse pniCredentialResponse;
return Optional.of(new Profile(
if (credentialRequest.isPresent() && credentialType.isPresent() && profile.isPresent() && requestAccount.isPresent()) {
if (PNI_CREDENTIAL_TYPE.equals(credentialType.get())) {
profileKeyCredentialResponse = null;
pniCredentialResponse = getPniCredential(credentialRequest.get(),
profile.get(),
requestAccount.get().getUuid(),
requestAccount.get().getPhoneNumberIdentifier());
} else if (PROFILE_KEY_CREDENTIAL_TYPE.equals(credentialType.get())) {
profileKeyCredentialResponse = getProfileCredential(credentialRequest.get(),
profile.get(),
requestAccount.get().getUuid());
pniCredentialResponse = null;
} else {
throw new BadRequestException();
}
} else {
profileKeyCredentialResponse = null;
pniCredentialResponse = null;
}
return new Profile(
name,
about,
aboutEmoji,
@ -290,11 +319,8 @@ public class ProfileController {
username.orElse(null),
null,
profileBadgeConverter.convert(acceptableLanguages, accountProfile.get().getBadges(), isSelf),
credential.orElse(null)));
} catch (InvalidInputException e) {
logger.info("Bad profile request", e);
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}
profileKeyCredentialResponse,
pniCredentialResponse);
}
@ -332,28 +358,37 @@ public class ProfileController {
getAcceptableLanguagesForRequest(containerRequestContext),
accountProfile.getBadges(),
isSelf),
null,
null);
}
private Optional<ProfileKeyCredentialResponse> getProfileCredential(Optional<String> encodedProfileCredentialRequest,
Optional<VersionedProfile> profile,
UUID uuid)
throws InvalidInputException
{
if (encodedProfileCredentialRequest.isEmpty()) return Optional.empty();
if (profile.isEmpty()) return Optional.empty();
private ProfileKeyCredentialResponse getProfileCredential(final String encodedProfileCredentialRequest,
final VersionedProfile profile,
final UUID uuid) {
try {
ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.get().getCommitment());
ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedProfileCredentialRequest.get()));
ProfileKeyCredentialResponse response = zkProfileOperations.issueProfileKeyCredential(request, uuid, commitment);
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.getCommitment());
final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedProfileCredentialRequest));
return Optional.of(response);
} catch (DecoderException | VerificationFailedException e) {
return zkProfileOperations.issueProfileKeyCredential(request, uuid, commitment);
} catch (DecoderException | VerificationFailedException | InvalidInputException e) {
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
}
}
private PniCredentialResponse getPniCredential(final String encodedCredentialRequest,
final VersionedProfile profile,
final UUID accountIdentifier,
final UUID phoneNumberIdentifier) {
try {
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.getCommitment());
final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedCredentialRequest));
return zkProfileOperations.issuePniCredential(request, accountIdentifier, phoneNumberIdentifier, commitment);
} catch (DecoderException | VerificationFailedException | InvalidInputException e) {
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
}
}
// Old profile endpoints. Replaced by versioned profile endpoints (above)
@ -415,6 +450,7 @@ public class ProfileController {
getAcceptableLanguagesForRequest(containerRequestContext),
accountProfile.get().getBadges(),
isSelf),
null,
null);
}

View File

@ -0,0 +1,41 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.Base64;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.PniCredentialResponse;
public class PniCredentialResponseAdapter {
public static class Serializing extends JsonSerializer<PniCredentialResponse> {
@Override
public void serialize(PniCredentialResponse response, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
if (response == null) jsonGenerator.writeNull();
else jsonGenerator.writeString(Base64.getEncoder().encodeToString(response.serialize()));
}
}
public static class Deserializing extends JsonDeserializer<PniCredentialResponse> {
@Override
public PniCredentialResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException {
try {
return new PniCredentialResponse(Base64.getDecoder().decode(jsonParser.getValueAsString()));
} catch (InvalidInputException e) {
throw new IOException(e);
}
}
}
}

View File

@ -11,6 +11,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.VisibleForTesting;
import java.util.List;
import java.util.UUID;
import org.signal.zkgroup.profiles.PniCredentialResponse;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
public class Profile {
@ -56,12 +57,17 @@ public class Profile {
@JsonDeserialize(using = ProfileKeyCredentialResponseAdapter.Deserializing.class)
private ProfileKeyCredentialResponse credential;
@JsonProperty
@JsonSerialize(using = PniCredentialResponseAdapter.Serializing.class)
@JsonDeserialize(using = PniCredentialResponseAdapter.Deserializing.class)
private PniCredentialResponse pniCredential;
public Profile() {}
public Profile(
String name, String about, String aboutEmoji, String avatar, String paymentAddress, String identityKey,
String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess, UserCapabilities capabilities, String username,
UUID uuid, List<Badge> badges, ProfileKeyCredentialResponse credential)
UUID uuid, List<Badge> badges, ProfileKeyCredentialResponse credential, PniCredentialResponse pniCredential)
{
this.name = name;
this.about = about;
@ -76,6 +82,7 @@ public class Profile {
this.uuid = uuid;
this.badges = badges;
this.credential = credential;
this.pniCredential = pniCredential;
}
@VisibleForTesting