Write certain profile data as bytes instead of strings to dynamo and represent those fields as byte arrays on VersionedProfile

This commit is contained in:
Katherine Yen
2023-08-16 13:45:16 -07:00
committed by GitHub
parent 33498cf147
commit 19a08f01e8
11 changed files with 438 additions and 405 deletions

View File

@@ -35,6 +35,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.BadRequestException;
@@ -176,15 +177,15 @@ public class ProfileController {
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream()
.anyMatch(prefix -> auth.getAccount().getNumber().startsWith(prefix));
if (hasDisallowedPrefix && currentProfile.map(VersionedProfile::getPaymentAddress).isEmpty()) {
if (hasDisallowedPrefix && currentProfile.map(VersionedProfile::paymentAddress).isEmpty()) {
return Response.status(Response.Status.FORBIDDEN).build();
}
}
Optional<String> currentAvatar = Optional.empty();
if (currentProfile.isPresent() && currentProfile.get().getAvatar() != null && currentProfile.get().getAvatar()
if (currentProfile.isPresent() && currentProfile.get().avatar() != null && currentProfile.get().avatar()
.startsWith("profiles/")) {
currentAvatar = Optional.of(currentProfile.get().getAvatar());
currentAvatar = Optional.of(currentProfile.get().avatar());
}
final String avatar = switch (request.getAvatarChange()) {
@@ -196,11 +197,11 @@ public class ProfileController {
profilesManager.set(auth.getAccount().getUuid(),
new VersionedProfile(
request.getVersion(),
request.getName(),
decodeFromBase64(request.getName()),
avatar,
request.getAboutEmoji(),
request.getAbout(),
request.getPaymentAddress(),
decodeFromBase64(request.getAboutEmoji()),
decodeFromBase64(request.getAbout()),
decodeFromBase64(request.getPaymentAddress()),
request.getCommitment().serialize()));
if (request.getAvatarChange() != CreateProfileRequest.AvatarChange.UNCHANGED) {
@@ -406,16 +407,17 @@ public class ProfileController {
VERSION_NOT_FOUND_COUNTER.increment();
}
final String name = maybeProfile.map(VersionedProfile::getName).orElse(null);
final String about = maybeProfile.map(VersionedProfile::getAbout).orElse(null);
final String aboutEmoji = maybeProfile.map(VersionedProfile::getAboutEmoji).orElse(null);
final String avatar = maybeProfile.map(VersionedProfile::getAvatar).orElse(null);
final String name = maybeProfile.map(VersionedProfile::name).map(ProfileController::encodeToBase64).orElse(null);
final String about = maybeProfile.map(VersionedProfile::about).map(ProfileController::encodeToBase64).orElse(null);
final String aboutEmoji = maybeProfile.map(VersionedProfile::aboutEmoji).map(ProfileController::encodeToBase64).orElse(null);
final String avatar = maybeProfile.map(VersionedProfile::avatar).orElse(null);
// Allow requests where either the version matches the latest version on Account or the latest version on Account
// is empty to read the payment address.
final String paymentAddress = maybeProfile
.filter(p -> account.getCurrentProfileVersion().map(v -> v.equals(version)).orElse(true))
.map(VersionedProfile::getPaymentAddress)
.map(VersionedProfile::paymentAddress)
.map(ProfileController::encodeToBase64)
.orElse(null);
return new VersionedProfileResponse(
@@ -454,7 +456,7 @@ public class ProfileController {
final Instant expiration) {
try {
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.getCommitment());
final ProfileKeyCommitment commitment = new ProfileKeyCommitment(profile.commitment());
final ProfileKeyCredentialRequest request = new ProfileKeyCredentialRequest(
HexFormat.of().parseHex(encodedCredentialRequest));
@@ -516,4 +518,20 @@ public class ProfileController {
private boolean isSelfProfileRequest(final Optional<Account> maybeRequester, final AciServiceIdentifier targetIdentifier) {
return maybeRequester.map(requester -> requester.getUuid().equals(targetIdentifier.uuid())).orElse(false);
}
@Nullable
private static byte[] decodeFromBase64(@Nullable final String input) {
if (input == null) {
return null;
}
return Base64.getDecoder().decode(input);
}
@Nullable
private static String encodeToBase64(@Nullable final byte[] input) {
if (input == null) {
return null;
}
return Base64.getEncoder().encodeToString(input);
}
}

View File

@@ -25,7 +25,6 @@ import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -85,14 +84,14 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase {
final boolean hasDisallowedPrefix =
dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream()
.anyMatch(prefix -> accountAndMaybeProfile.getT1().getNumber().startsWith(prefix));
if (hasDisallowedPrefix && accountAndMaybeProfile.getT2().map(VersionedProfile::getPaymentAddress).isEmpty()) {
if (hasDisallowedPrefix && accountAndMaybeProfile.getT2().map(VersionedProfile::paymentAddress).isEmpty()) {
throw Status.PERMISSION_DENIED.asRuntimeException();
}
}
})
.flatMap(accountAndMaybeProfile -> {
final Account account = accountAndMaybeProfile.getT1();
final Optional<String> currentAvatar = accountAndMaybeProfile.getT2().map(VersionedProfile::getAvatar)
final Optional<String> currentAvatar = accountAndMaybeProfile.getT2().map(VersionedProfile::avatar)
.filter(avatar -> avatar.startsWith("profiles/"));
final AvatarData avatarData = switch (AvatarChangeUtil.fromGrpcAvatarChange(request.getAvatarChange())) {
case AVATAR_CHANGE_UNCHANGED -> new AvatarData(currentAvatar, currentAvatar, Optional.empty());
@@ -107,11 +106,11 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase {
final Mono<Void> profileSetMono = Mono.fromFuture(profilesManager.setAsync(account.getUuid(),
new VersionedProfile(
request.getVersion(),
encodeToBase64(request.getName().toByteArray()),
request.getName().toByteArray(),
avatarData.finalAvatar().orElse(null),
encodeToBase64(request.getAboutEmoji().toByteArray()),
encodeToBase64(request.getAbout().toByteArray()),
encodeToBase64(request.getPaymentAddress().toByteArray()),
request.getAboutEmoji().toByteArray(),
request.getAbout().toByteArray(),
request.getPaymentAddress().toByteArray(),
request.getCommitment().toByteArray())));
final List<Mono<?>> updates = new ArrayList<>(2);
@@ -163,8 +162,4 @@ public class ProfileGrpcService extends ReactorProfileGrpc.ProfileImplBase {
throw Status.INVALID_ARGUMENT.withDescription(errorMessage).asRuntimeException();
}
private static String encodeToBase64(byte[] input) {
return Base64.getEncoder().encodeToString(input);
}
}

View File

@@ -11,7 +11,6 @@ import com.google.common.annotations.VisibleForTesting;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Timer;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -47,19 +46,19 @@ public class Profiles {
@VisibleForTesting
static final String ATTR_VERSION = "V";
// User's name; string
// User's name; byte array
private static final String ATTR_NAME = "N";
// Avatar path/filename; string
private static final String ATTR_AVATAR = "A";
// Bio/about text; string
// Bio/about text; byte array
private static final String ATTR_ABOUT = "B";
// Bio/about emoji; string
// Bio/about emoji; byte array
private static final String ATTR_EMOJI = "E";
// Payment address; string
// Payment address; byte array
private static final String ATTR_PAYMENT_ADDRESS = "P";
// Commitment; byte array
@@ -91,7 +90,7 @@ public class Profiles {
SET_PROFILES_TIMER.record(() -> {
dynamoDbClient.updateItem(UpdateItemRequest.builder()
.tableName(tableName)
.key(buildPrimaryKey(uuid, profile.getVersion()))
.key(buildPrimaryKey(uuid, profile.version()))
.updateExpression(buildUpdateExpression(profile))
.expressionAttributeNames(UPDATE_EXPRESSION_ATTRIBUTE_NAMES)
.expressionAttributeValues(buildUpdateExpressionAttributeValues(profile))
@@ -102,7 +101,7 @@ public class Profiles {
public CompletableFuture<Void> setAsync(final UUID uuid, final VersionedProfile profile) {
return AsyncTimerUtil.record(SET_PROFILES_TIMER, () -> dynamoDbAsyncClient.updateItem(UpdateItemRequest.builder()
.tableName(tableName)
.key(buildPrimaryKey(uuid, profile.getVersion()))
.key(buildPrimaryKey(uuid, profile.version()))
.updateExpression(buildUpdateExpression(profile))
.expressionAttributeNames(UPDATE_EXPRESSION_ATTRIBUTE_NAMES)
.expressionAttributeValues(buildUpdateExpressionAttributeValues(profile))
@@ -122,31 +121,31 @@ public class Profiles {
final List<String> updatedAttributes = new ArrayList<>(5);
final List<String> deletedAttributes = new ArrayList<>(5);
if (StringUtils.isNotBlank(profile.getName())) {
if (profile.name() != null) {
updatedAttributes.add("name");
} else {
deletedAttributes.add("name");
}
if (StringUtils.isNotBlank(profile.getAvatar())) {
if (StringUtils.isNotBlank(profile.avatar())) {
updatedAttributes.add("avatar");
} else {
deletedAttributes.add("avatar");
}
if (StringUtils.isNotBlank(profile.getAbout())) {
if (profile.about() != null) {
updatedAttributes.add("about");
} else {
deletedAttributes.add("about");
}
if (StringUtils.isNotBlank(profile.getAboutEmoji())) {
if (profile.aboutEmoji() != null) {
updatedAttributes.add("aboutEmoji");
} else {
deletedAttributes.add("aboutEmoji");
}
if (StringUtils.isNotBlank(profile.getPaymentAddress())) {
if (profile.paymentAddress() != null) {
updatedAttributes.add("paymentAddress");
} else {
deletedAttributes.add("paymentAddress");
@@ -177,26 +176,26 @@ public class Profiles {
static Map<String, AttributeValue> buildUpdateExpressionAttributeValues(final VersionedProfile profile) {
final Map<String, AttributeValue> expressionValues = new HashMap<>();
expressionValues.put(":commitment", AttributeValues.fromByteArray(profile.getCommitment()));
expressionValues.put(":commitment", AttributeValues.fromByteArray(profile.commitment()));
if (StringUtils.isNotBlank(profile.getName())) {
expressionValues.put(":name", AttributeValues.fromString(profile.getName()));
if (profile.name() != null) {
expressionValues.put(":name", AttributeValues.fromByteArray(profile.name()));
}
if (StringUtils.isNotBlank(profile.getAvatar())) {
expressionValues.put(":avatar", AttributeValues.fromString(profile.getAvatar()));
if (StringUtils.isNotBlank(profile.avatar())) {
expressionValues.put(":avatar", AttributeValues.fromString(profile.avatar()));
}
if (StringUtils.isNotBlank(profile.getAbout())) {
expressionValues.put(":about", AttributeValues.fromString(profile.getAbout()));
if (profile.about() != null) {
expressionValues.put(":about", AttributeValues.fromByteArray(profile.about()));
}
if (StringUtils.isNotBlank(profile.getAboutEmoji())) {
expressionValues.put(":aboutEmoji", AttributeValues.fromString(profile.getAboutEmoji()));
if (profile.aboutEmoji() != null) {
expressionValues.put(":aboutEmoji", AttributeValues.fromByteArray(profile.aboutEmoji()));
}
if (StringUtils.isNotBlank(profile.getPaymentAddress())) {
expressionValues.put(":paymentAddress", AttributeValues.fromString(profile.getPaymentAddress()));
if (profile.paymentAddress() != null) {
expressionValues.put(":paymentAddress", AttributeValues.fromByteArray(profile.paymentAddress()));
}
return expressionValues;
@@ -228,21 +227,21 @@ public class Profiles {
private static VersionedProfile fromItem(final Map<String, AttributeValue> item) {
return new VersionedProfile(
AttributeValues.getString(item, ATTR_VERSION, null),
getBase64EncodedBytes(item, ATTR_NAME, PARSE_BYTE_ARRAY_COUNTER_NAME),
getBytes(item, ATTR_NAME),
AttributeValues.getString(item, ATTR_AVATAR, null),
getBase64EncodedBytes(item, ATTR_EMOJI, PARSE_BYTE_ARRAY_COUNTER_NAME),
getBase64EncodedBytes(item, ATTR_ABOUT, PARSE_BYTE_ARRAY_COUNTER_NAME),
getBase64EncodedBytes(item, ATTR_PAYMENT_ADDRESS, PARSE_BYTE_ARRAY_COUNTER_NAME),
getBytes(item, ATTR_EMOJI),
getBytes(item, ATTR_ABOUT),
getBytes(item, ATTR_PAYMENT_ADDRESS),
AttributeValues.getByteArray(item, ATTR_COMMITMENT, null));
}
private static String getBase64EncodedBytes(final Map<String, AttributeValue> item, final String attributeName, final String counterName) {
private static byte[] getBytes(final Map<String, AttributeValue> item, final String attributeName) {
final AttributeValue attributeValue = item.get(attributeName);
if (attributeValue == null) {
return null;
}
return Base64.getEncoder().encodeToString(AttributeValues.extractByteArray(attributeValue, counterName));
return AttributeValues.extractByteArray(attributeValue, PARSE_BYTE_ARRAY_COUNTER_NAME);
}
public void deleteAll(final UUID uuid) {

View File

@@ -77,7 +77,7 @@ public class ProfilesManager {
try {
final String profileJson = mapper.writeValueAsString(profile);
cacheCluster.useCluster(connection -> connection.sync().hset(getCacheKey(uuid), profile.getVersion(), profileJson));
cacheCluster.useCluster(connection -> connection.sync().hset(getCacheKey(uuid), profile.version(), profileJson));
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e);
}
@@ -93,7 +93,7 @@ public class ProfilesManager {
}
return cacheCluster.withCluster(connection ->
connection.async().hset(getCacheKey(uuid), profile.getVersion(), profileJson))
connection.async().hset(getCacheKey(uuid), profile.version(), profileJson))
.thenRun(Util.NOOP)
.toCompletableFuture();
}

View File

@@ -10,84 +10,29 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter;
import java.util.Arrays;
import java.util.Objects;
public class VersionedProfile {
public record VersionedProfile (String version,
@JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class)
byte[] name,
private final String version;
private final String name;
private final String avatar;
private final String aboutEmoji;
private final String about;
private final String paymentAddress;
String avatar,
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
private byte[] commitment;
@JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class)
byte[] aboutEmoji,
@JsonCreator
public VersionedProfile(
@JsonProperty("version") final String version,
@JsonProperty("name") final String name,
@JsonProperty("avatar") final String avatar,
@JsonProperty("aboutEmoji") final String aboutEmoji,
@JsonProperty("about") final String about,
@JsonProperty("paymentAddress") final String paymentAddress,
@JsonProperty("commitment") final byte[] commitment) {
this.version = version;
this.name = name;
this.avatar = avatar;
this.aboutEmoji = aboutEmoji;
this.about = about;
this.paymentAddress = paymentAddress;
this.commitment = commitment;
}
@JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class)
byte[] about,
public String getVersion() {
return version;
}
@JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class)
byte[] paymentAddress,
public String getName() {
return name;
}
public String getAvatar() {
return avatar;
}
public String getAboutEmoji() {
return aboutEmoji;
}
public String getAbout() {
return about;
}
public String getPaymentAddress() {
return paymentAddress;
}
public byte[] getCommitment() {
return commitment;
}
@Override
public boolean equals(final Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
final VersionedProfile that = (VersionedProfile) o;
return Objects.equals(version, that.version) && Objects.equals(name, that.name) && Objects.equals(avatar,
that.avatar) && Objects.equals(aboutEmoji, that.aboutEmoji) && Objects.equals(about, that.about)
&& Objects.equals(paymentAddress, that.paymentAddress) && Arrays.equals(commitment, that.commitment);
}
@Override
public int hashCode() {
int result = Objects.hash(version, name, avatar, aboutEmoji, about, paymentAddress);
result = 31 * result + Arrays.hashCode(commitment);
return result;
}
}
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
byte[] commitment) {}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.textsecuregcm.util;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import java.util.Base64;
public class ByteArrayBase64WithPaddingAdapter {
public static class Serializing extends JsonSerializer<byte[]> {
@Override
public void serialize(byte[] bytes, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
throws IOException {
jsonGenerator.writeString(Base64.getEncoder().encodeToString(bytes));
}
}
public static class Deserializing extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
return Base64.getDecoder().decode(jsonParser.getValueAsString());
}
}
}