mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 19:48:01 +01:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user