diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java index 6426eccb1..6e2db8eb8 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -55,7 +55,6 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.apache.commons.lang3.StringUtils; import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.ServiceId; import org.signal.libsignal.zkgroup.InvalidInputException; @@ -81,7 +80,6 @@ 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.util.ProfileHelper; import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; @@ -97,6 +95,7 @@ import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.storage.ProfilesManager; import org.whispersystems.textsecuregcm.storage.VersionedProfile; import org.whispersystems.textsecuregcm.util.Pair; +import org.whispersystems.textsecuregcm.util.ProfileHelper; import org.whispersystems.textsecuregcm.util.Util; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; @@ -166,7 +165,7 @@ public class ProfileController { final Optional currentProfile = profilesManager.get(auth.getAccount().getUuid(), request.getVersion()); - if (StringUtils.isNotBlank(request.getPaymentAddress())) { + if (request.getPaymentAddress() != null && request.getPaymentAddress().length != 0) { final boolean hasDisallowedPrefix = dynamicConfigurationManager.getConfiguration().getPaymentsConfiguration().getDisallowedPrefixes().stream() .anyMatch(prefix -> auth.getAccount().getNumber().startsWith(prefix)); @@ -191,11 +190,11 @@ public class ProfileController { profilesManager.set(auth.getAccount().getUuid(), new VersionedProfile( request.getVersion(), - decodeFromBase64(request.getName()), + request.getName(), avatar, - decodeFromBase64(request.getAboutEmoji()), - decodeFromBase64(request.getAbout()), - decodeFromBase64(request.getPaymentAddress()), + request.getAboutEmoji(), + request.getAbout(), + request.getPaymentAddress(), request.getCommitment().serialize())); if (request.getAvatarChange() != CreateProfileRequest.AvatarChange.UNCHANGED) { @@ -408,17 +407,16 @@ public class ProfileController { VERSION_NOT_FOUND_COUNTER.increment(); } - 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 byte[] name = maybeProfile.map(VersionedProfile::name).orElse(null); + final byte[] about = maybeProfile.map(VersionedProfile::about).orElse(null); + final byte[] aboutEmoji = maybeProfile.map(VersionedProfile::aboutEmoji).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 + final byte[] paymentAddress = maybeProfile .filter(p -> account.getCurrentProfileVersion().map(v -> v.equals(version)).orElse(true)) .map(VersionedProfile::paymentAddress) - .map(ProfileController::encodeToBase64) .orElse(null); return new VersionedProfileResponse( diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java index e98e9e668..068a6a5a2 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateProfileRequest.java @@ -13,8 +13,8 @@ import java.util.Optional; import javax.annotation.Nullable; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; -import org.apache.commons.lang3.StringUtils; import org.signal.libsignal.zkgroup.profiles.ProfileKeyCommitment; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; import org.whispersystems.textsecuregcm.util.ExactlySize; public class CreateProfileRequest { @@ -24,8 +24,10 @@ public class CreateProfileRequest { private String version; @JsonProperty - @ExactlySize({108, 380}) - private String name; + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + @ExactlySize({81, 285}) + private byte[] name; @JsonProperty private boolean avatar; @@ -34,16 +36,22 @@ public class CreateProfileRequest { private boolean sameAvatar; @JsonProperty - @ExactlySize({0, 80}) - private String aboutEmoji; + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + @ExactlySize({0, 60}) + private byte[] aboutEmoji; @JsonProperty - @ExactlySize({0, 208, 376, 720}) - private String about; + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + @ExactlySize({0, 156, 282, 540}) + private byte[] about; @JsonProperty - @ExactlySize({0, 776}) - private String paymentAddress; + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + @ExactlySize({0, 582}) + private byte[] paymentAddress; @JsonProperty @Nullable @@ -59,8 +67,8 @@ public class CreateProfileRequest { } public CreateProfileRequest( - ProfileKeyCommitment commitment, String version, String name, String aboutEmoji, String about, - String paymentAddress, boolean wantsAvatar, boolean sameAvatar, List badgeIds) { + final ProfileKeyCommitment commitment, final String version, final byte[] name, final byte[] aboutEmoji, final byte[] about, + final byte[] paymentAddress, final boolean wantsAvatar, final boolean sameAvatar, final List badgeIds) { this.commitment = commitment; this.version = version; this.name = name; @@ -80,7 +88,7 @@ public class CreateProfileRequest { return version; } - public String getName() { + public byte[] getName() { return name; } @@ -104,16 +112,16 @@ public class CreateProfileRequest { return AvatarChange.UNCHANGED; } - public String getAboutEmoji() { - return StringUtils.stripToNull(aboutEmoji); + public byte[] getAboutEmoji() { + return aboutEmoji; } - public String getAbout() { - return StringUtils.stripToNull(about); + public byte[] getAbout() { + return about; } - public String getPaymentAddress() { - return StringUtils.stripToNull(paymentAddress); + public byte[] getPaymentAddress() { + return paymentAddress; } public Optional> getBadges() { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java index 30e71a101..3b8ed60dc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/VersionedProfileResponse.java @@ -7,6 +7,9 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.whispersystems.textsecuregcm.util.ByteArrayBase64WithPaddingAdapter; public class VersionedProfileResponse { @@ -14,29 +17,37 @@ public class VersionedProfileResponse { private BaseProfileResponse baseProfileResponse; @JsonProperty - private String name; + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + private byte[] name; @JsonProperty - private String about; + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + private byte[] about; @JsonProperty - private String aboutEmoji; + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + private byte[] aboutEmoji; @JsonProperty private String avatar; @JsonProperty - private String paymentAddress; + @JsonSerialize(using = ByteArrayBase64WithPaddingAdapter.Serializing.class) + @JsonDeserialize(using = ByteArrayBase64WithPaddingAdapter.Deserializing.class) + private byte[] paymentAddress; public VersionedProfileResponse() { } public VersionedProfileResponse(final BaseProfileResponse baseProfileResponse, - final String name, - final String about, - final String aboutEmoji, + final byte[] name, + final byte[] about, + final byte[] aboutEmoji, final String avatar, - final String paymentAddress) { + final byte[] paymentAddress) { this.baseProfileResponse = baseProfileResponse; this.name = name; @@ -50,15 +61,15 @@ public class VersionedProfileResponse { return baseProfileResponse; } - public String getName() { + public byte[] getName() { return name; } - public String getAbout() { + public byte[] getAbout() { return about; } - public String getAboutEmoji() { + public byte[] getAboutEmoji() { return aboutEmoji; } @@ -66,7 +77,7 @@ public class VersionedProfileResponse { return avatar; } - public String getPaymentAddress() { + public byte[] getPaymentAddress() { return paymentAddress; } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java index a8902242a..56fd77b63 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/ProfileControllerTest.java @@ -417,7 +417,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .put(Entity.entity(new CreateProfileRequest(commitment, "someversion", - ProfileTestHelper.encodeToBase64(name), null, null, + name, null, null, null, true, false, List.of()), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); @@ -438,7 +438,7 @@ class ProfileControllerTest { @Test void testSetProfileWantAvatarUploadWithBadProfileSize() throws InvalidInputException { final ProfileKeyCommitment commitment = new ProfileKey(new byte[32]).getCommitment(new ServiceId.Aci(AuthHelper.VALID_UUID)); - final String name = ProfileTestHelper.generateRandomBase64FromByteArray(82); + final byte[] name = ProfileTestHelper.generateRandomByteArray(82); try (final Response response = resources.getJerseyTest() .target("/v1/profile/") @@ -462,7 +462,7 @@ class ProfileControllerTest { .target("/v1/profile/") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) - .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", ProfileTestHelper.encodeToBase64(name), null, null, + .put(Entity.entity(new CreateProfileRequest(commitment, "anotherversion", name, null, null, null, false, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); @@ -494,7 +494,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", - ProfileTestHelper.encodeToBase64(name), null, null, + name, null, null, null, true, false, List.of()), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); final ArgumentCaptor profileArgumentCaptor = ArgumentCaptor.forClass(VersionedProfile.class); @@ -520,7 +520,7 @@ class ProfileControllerTest { .target("/v1/profile/") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) - .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", ProfileTestHelper.encodeToBase64(name), + .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, null, null, null, false, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); @@ -550,7 +550,7 @@ class ProfileControllerTest { .target("/v1/profile/") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) - .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", ProfileTestHelper.encodeToBase64(name), + .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, null, null, null, true, true, List.of()), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); @@ -580,7 +580,7 @@ class ProfileControllerTest { .target("/v1/profile/") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) - .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", ProfileTestHelper.encodeToBase64(name), + .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, null, null, null, false, true, List.of()), MediaType.APPLICATION_JSON_TYPE))) { @@ -608,7 +608,7 @@ class ProfileControllerTest { .target("/v1/profile/") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) - .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", ProfileTestHelper.encodeToBase64(name), + .put(Entity.entity(new CreateProfileRequest(commitment, "validversion", name, null, null, null, true, true, List.of()), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); @@ -640,7 +640,7 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity( - new CreateProfileRequest(commitment, "validversion", ProfileTestHelper.encodeToBase64(name), + new CreateProfileRequest(commitment, "validversion", name, null, null, null, true, false, List.of()), MediaType.APPLICATION_JSON_TYPE), ProfileAvatarUploadAttributes.class); @@ -673,8 +673,8 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity( - new CreateProfileRequest(commitment, "anotherversion", ProfileTestHelper.encodeToBase64(name), - ProfileTestHelper.encodeToBase64(emoji), ProfileTestHelper.encodeToBase64(about), null, false, false, List.of()), + new CreateProfileRequest(commitment, "anotherversion", name, emoji, about, null, + false, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); @@ -712,8 +712,8 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity( - new CreateProfileRequest(commitment, "yetanotherversion", ProfileTestHelper.encodeToBase64(name), - null, null, ProfileTestHelper.encodeToBase64(paymentAddress), false, false, + new CreateProfileRequest(commitment, "yetanotherversion", name, + null, null, paymentAddress, false, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { assertThat(response.getStatus()).isEqualTo(200); @@ -746,8 +746,8 @@ class ProfileControllerTest { clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); - final String name = ProfileTestHelper.generateRandomBase64FromByteArray(81); - final String paymentAddress = ProfileTestHelper.generateRandomBase64FromByteArray(582); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); try (final Response response = resources.getJerseyTest() .target("/v1/profile") @@ -790,8 +790,8 @@ class ProfileControllerTest { .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID_TWO, AuthHelper.VALID_PASSWORD_TWO)) .put(Entity.entity( - new CreateProfileRequest(commitment, "yetanotherversion", ProfileTestHelper.encodeToBase64(name), - null, null, ProfileTestHelper.encodeToBase64(paymentAddress), false, false, + new CreateProfileRequest(commitment, "yetanotherversion", name, + null, null, paymentAddress, false, false, List.of()), MediaType.APPLICATION_JSON_TYPE))) { if (existingPaymentAddressOnProfile) { @@ -838,9 +838,9 @@ class ProfileControllerTest { .get(VersionedProfileResponse.class); assertThat(profile.getBaseProfileResponse().getIdentityKey()).isEqualTo(ACCOUNT_TWO_IDENTITY_KEY); - assertThat(profile.getName()).isEqualTo(ProfileTestHelper.encodeToBase64(name)); - assertThat(profile.getAbout()).isEqualTo(ProfileTestHelper.encodeToBase64(about)); - assertThat(profile.getAboutEmoji()).isEqualTo(ProfileTestHelper.encodeToBase64(emoji)); + assertThat(profile.getName()).containsExactly(name); + assertThat(profile.getAbout()).containsExactly(about); + assertThat(profile.getAboutEmoji()).containsExactly(emoji); assertThat(profile.getAvatar()).isEqualTo("profiles/validavatar"); assertThat(profile.getBaseProfileResponse().getCapabilities().gv1Migration()).isTrue(); assertThat(profile.getBaseProfileResponse().getUuid()).isEqualTo(new AciServiceIdentifier(AuthHelper.VALID_UUID_TWO)); @@ -859,8 +859,8 @@ class ProfileControllerTest { clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); - final String name = ProfileTestHelper.generateRandomBase64FromByteArray(81); - final String paymentAddress = ProfileTestHelper.generateRandomBase64FromByteArray(582); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] paymentAddress = ProfileTestHelper.generateRandomByteArray(582); try (final Response response = resources.getJerseyTest() .target("/v1/profile") @@ -890,7 +890,7 @@ class ProfileControllerTest { .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .get(VersionedProfileResponse.class); - assertThat(profile.getPaymentAddress()).isEqualTo(ProfileTestHelper.encodeToBase64(paymentAddress)); + assertThat(profile.getPaymentAddress()).containsExactly(paymentAddress); } when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.of("validversion")); @@ -902,7 +902,7 @@ class ProfileControllerTest { .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD)) .get(VersionedProfileResponse.class); - assertThat(profile.getPaymentAddress()).isEqualTo(ProfileTestHelper.encodeToBase64(paymentAddress)); + assertThat(profile.getPaymentAddress()).containsExactly(paymentAddress); } when(profileAccount.getCurrentProfileVersion()).thenReturn(Optional.of("someotherversion")); @@ -949,9 +949,9 @@ class ProfileControllerTest { clearInvocations(AuthHelper.VALID_ACCOUNT_TWO); - final String name = ProfileTestHelper.generateRandomBase64FromByteArray(81); - final String emoji = ProfileTestHelper.generateRandomBase64FromByteArray(60); - final String about = ProfileTestHelper.generateRandomBase64FromByteArray(156); + final byte[] name = ProfileTestHelper.generateRandomByteArray(81); + final byte[] emoji = ProfileTestHelper.generateRandomByteArray(60); + final byte[] about = ProfileTestHelper.generateRandomByteArray(156); try (final Response response = resources.getJerseyTest() .target("/v1/profile/")