Allow callers to get an expiring profile key credential
This commit is contained in:
parent
e38e5fa17d
commit
38e30c7513
|
@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.controllers;
|
|||
import static org.whispersystems.textsecuregcm.metrics.MetricsUtil.name;
|
||||
|
||||
import com.codahale.metrics.annotation.Timed;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Preconditions;
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
|
@ -19,8 +20,10 @@ import java.security.NoSuchAlgorithmException;
|
|||
import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
@ -66,6 +69,7 @@ import org.apache.commons.codec.binary.Hex;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.profiles.PniCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||
|
@ -86,6 +90,7 @@ import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckRequest;
|
|||
import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.CredentialProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ExpiringProfileKeyCredentialProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PniCredentialProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileKeyCredentialProfileResponse;
|
||||
|
@ -129,8 +134,12 @@ public class ProfileController {
|
|||
|
||||
private final Executor batchIdentityCheckExecutor;
|
||||
|
||||
@VisibleForTesting
|
||||
static final Duration EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION = Duration.ofDays(7);
|
||||
|
||||
private static final String PROFILE_KEY_CREDENTIAL_TYPE = "profileKey";
|
||||
private static final String PNI_CREDENTIAL_TYPE = "pni";
|
||||
private static final String EXPIRING_PROFILE_KEY_CREDENTIAL_TYPE = "expiringProfileKey";
|
||||
|
||||
private static final Counter VERSION_NOT_FOUND_COUNTER = Metrics.counter(name(ProfileController.class, "versionNotFound"));
|
||||
private static final String INVALID_ACCEPT_LANGUAGE_COUNTER_NAME = name(ProfileController.class, "invalidAcceptLanguage");
|
||||
|
@ -285,6 +294,15 @@ public class ProfileController {
|
|||
containerRequestContext);
|
||||
}
|
||||
|
||||
case EXPIRING_PROFILE_KEY_CREDENTIAL_TYPE -> {
|
||||
return buildExpiringProfileKeyCredentialProfileResponse(targetAccount,
|
||||
version,
|
||||
credentialRequest,
|
||||
isSelf,
|
||||
Instant.now().plus(EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION).truncatedTo(ChronoUnit.DAYS),
|
||||
containerRequestContext);
|
||||
}
|
||||
|
||||
default -> throw new BadRequestException();
|
||||
}
|
||||
}
|
||||
|
@ -415,6 +433,23 @@ public class ProfileController {
|
|||
pniCredentialResponse);
|
||||
}
|
||||
|
||||
private ExpiringProfileKeyCredentialProfileResponse buildExpiringProfileKeyCredentialProfileResponse(
|
||||
final Account account,
|
||||
final String version,
|
||||
final String encodedCredentialRequest,
|
||||
final boolean isSelf,
|
||||
final Instant expiration,
|
||||
final ContainerRequestContext containerRequestContext) {
|
||||
|
||||
final ExpiringProfileKeyCredentialResponse expiringProfileKeyCredentialResponse = profilesManager.get(account.getUuid(), version)
|
||||
.map(profile -> getExpiringProfileKeyCredentialResponse(encodedCredentialRequest, profile, account.getUuid(), expiration))
|
||||
.orElse(null);
|
||||
|
||||
return new ExpiringProfileKeyCredentialProfileResponse(
|
||||
buildVersionedProfileResponse(account, version, isSelf, containerRequestContext),
|
||||
expiringProfileKeyCredentialResponse);
|
||||
}
|
||||
|
||||
private VersionedProfileResponse buildVersionedProfileResponse(final Account account,
|
||||
final String version,
|
||||
final boolean isSelf,
|
||||
|
@ -514,6 +549,22 @@ public class ProfileController {
|
|||
}
|
||||
}
|
||||
|
||||
private ExpiringProfileKeyCredentialResponse getExpiringProfileKeyCredentialResponse(
|
||||
final String encodedCredentialRequest,
|
||||
final VersionedProfile profile,
|
||||
final UUID accountIdentifier,
|
||||
final Instant expiration) {
|
||||
|
||||
try {
|
||||
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.getCommitment());
|
||||
final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(Hex.decodeHex(encodedCredentialRequest));
|
||||
|
||||
return zkProfileOperations.issueExpiringProfileKeyCredential(request, accountIdentifier, commitment, expiration);
|
||||
} catch (DecoderException | VerificationFailedException | InvalidInputException e) {
|
||||
throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).build());
|
||||
}
|
||||
}
|
||||
|
||||
private ProfileAvatarUploadAttributes generateAvatarUploadForm(String objectName) {
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
Pair<String, String> policy = policyGenerator.createFor(now, objectName, 10 * 1024 * 1024);
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2013-2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
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 javax.annotation.Nullable;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
|
||||
|
||||
public class ExpiringProfileKeyCredentialProfileResponse extends CredentialProfileResponse {
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ExpiringProfileKeyCredentialResponseAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ExpiringProfileKeyCredentialResponseAdapter.Deserializing.class)
|
||||
@Nullable
|
||||
private ExpiringProfileKeyCredentialResponse credential;
|
||||
|
||||
public ExpiringProfileKeyCredentialProfileResponse() {
|
||||
}
|
||||
|
||||
public ExpiringProfileKeyCredentialProfileResponse(final VersionedProfileResponse versionedProfileResponse,
|
||||
@Nullable final ExpiringProfileKeyCredentialResponse credential) {
|
||||
|
||||
super(versionedProfileResponse);
|
||||
this.credential = credential;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ExpiringProfileKeyCredentialResponse getCredential() {
|
||||
return credential;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2013-2022 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.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
|
||||
|
||||
public class ExpiringProfileKeyCredentialResponseAdapter {
|
||||
|
||||
public static class Serializing extends JsonSerializer<ExpiringProfileKeyCredentialResponse> {
|
||||
@Override
|
||||
public void serialize(ExpiringProfileKeyCredentialResponse 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<ExpiringProfileKeyCredentialResponse> {
|
||||
@Override
|
||||
public ExpiringProfileKeyCredentialResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||
throws IOException {
|
||||
try {
|
||||
return new ExpiringProfileKeyCredentialResponse(Base64.getDecoder().decode(jsonParser.getValueAsString()));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
/*
|
||||
* Copyright 2013-2021 Signal Messenger, LLC
|
||||
* Copyright 2013-2022 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
@ -31,6 +31,7 @@ import java.security.SecureRandom;
|
|||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
@ -64,6 +65,7 @@ import org.signal.libsignal.zkgroup.ServerPublicParams;
|
|||
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.profiles.PniCredentialResponse;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment;
|
||||
|
@ -86,6 +88,7 @@ import org.whispersystems.textsecuregcm.entities.BaseProfileResponse;
|
|||
import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.BatchIdentityCheckResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.CreateProfileRequest;
|
||||
import org.whispersystems.textsecuregcm.entities.ExpiringProfileKeyCredentialProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PniCredentialProfileResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.ProfileKeyCredentialProfileResponse;
|
||||
|
@ -1131,6 +1134,76 @@ class ProfileControllerTest {
|
|||
verify(zkProfileOperations, never()).issuePniCredential(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource
|
||||
void testGetProfileWithExpiringProfileKeyCredential(final MultivaluedMap<String, Object> authHeaders)
|
||||
throws VerificationFailedException, InvalidInputException {
|
||||
final String version = "version";
|
||||
final byte[] unidentifiedAccessKey = "test-uak".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
final ServerSecretParams serverSecretParams = ServerSecretParams.generate();
|
||||
final ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
|
||||
|
||||
final ServerZkProfileOperations serverZkProfile = new ServerZkProfileOperations(serverSecretParams);
|
||||
final ClientZkProfileOperations clientZkProfile = new ClientZkProfileOperations(serverPublicParams);
|
||||
|
||||
final byte[] profileKeyBytes = new byte[32];
|
||||
new SecureRandom().nextBytes(profileKeyBytes);
|
||||
|
||||
final ProfileKey profileKey = new ProfileKey(profileKeyBytes);
|
||||
final ProfileKeyCommitment profileKeyCommitment = profileKey.getCommitment(AuthHelper.VALID_UUID);
|
||||
|
||||
final VersionedProfile versionedProfile = mock(VersionedProfile.class);
|
||||
when(versionedProfile.getCommitment()).thenReturn(profileKeyCommitment.serialize());
|
||||
|
||||
final ProfileKeyCredentialRequestContext profileKeyCredentialRequestContext =
|
||||
clientZkProfile.createProfileKeyCredentialRequestContext(AuthHelper.VALID_UUID, profileKey);
|
||||
|
||||
final ProfileKeyCredentialRequest credentialRequest = profileKeyCredentialRequestContext.getRequest();
|
||||
|
||||
final Account account = mock(Account.class);
|
||||
when(account.getUuid()).thenReturn(AuthHelper.VALID_UUID);
|
||||
when(account.getCurrentProfileVersion()).thenReturn(Optional.of(version));
|
||||
when(account.isEnabled()).thenReturn(true);
|
||||
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
|
||||
|
||||
final Instant expiration = Instant.now().plus(ProfileController.EXPIRING_PROFILE_KEY_CREDENTIAL_EXPIRATION)
|
||||
.truncatedTo(ChronoUnit.DAYS);
|
||||
|
||||
final ExpiringProfileKeyCredentialResponse credentialResponse =
|
||||
serverZkProfile.issueExpiringProfileKeyCredential(credentialRequest, AuthHelper.VALID_UUID, profileKeyCommitment, expiration);
|
||||
|
||||
when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account));
|
||||
when(profilesManager.get(AuthHelper.VALID_UUID, version)).thenReturn(Optional.of(versionedProfile));
|
||||
when(zkProfileOperations.issueExpiringProfileKeyCredential(credentialRequest, AuthHelper.VALID_UUID, profileKeyCommitment, expiration))
|
||||
.thenReturn(credentialResponse);
|
||||
|
||||
final ExpiringProfileKeyCredentialProfileResponse profile = resources.getJerseyTest()
|
||||
.target(String.format("/v1/profile/%s/%s/%s", AuthHelper.VALID_UUID, version, Hex.encodeHexString(credentialRequest.serialize())))
|
||||
.queryParam("credentialType", "expiringProfileKey")
|
||||
.request()
|
||||
.headers(authHeaders)
|
||||
.get(ExpiringProfileKeyCredentialProfileResponse.class);
|
||||
|
||||
assertThat(profile.getVersionedProfileResponse().getBaseProfileResponse().getUuid()).isEqualTo(AuthHelper.VALID_UUID);
|
||||
assertThat(profile.getCredential()).isEqualTo(credentialResponse);
|
||||
|
||||
verify(zkProfileOperations).issueExpiringProfileKeyCredential(credentialRequest, AuthHelper.VALID_UUID, profileKeyCommitment, expiration);
|
||||
verify(zkProfileOperations, never()).issuePniCredential(any(), any(), any(), any());
|
||||
|
||||
final ClientZkProfileOperations clientZkProfileCipher = new ClientZkProfileOperations(serverPublicParams);
|
||||
assertThatNoException().isThrownBy(() ->
|
||||
clientZkProfileCipher.receiveExpiringProfileKeyCredential(profileKeyCredentialRequestContext, profile.getCredential()));
|
||||
}
|
||||
|
||||
private static Stream<Arguments> testGetProfileWithExpiringProfileKeyCredential() {
|
||||
return Stream.of(
|
||||
Arguments.of(new MultivaluedHashMap<>(Map.of(OptionalAccess.UNIDENTIFIED, Base64.getEncoder().encodeToString(UNIDENTIFIED_ACCESS_KEY)))),
|
||||
Arguments.of(new MultivaluedHashMap<>(Map.of("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)))),
|
||||
Arguments.of(new MultivaluedHashMap<>(Map.of("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO))))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetProfileWithPniCredentialVersionNotFound() throws VerificationFailedException {
|
||||
final Account account = mock(Account.class);
|
Loading…
Reference in New Issue