diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index 83227408f..6f2435739 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -55,6 +55,7 @@ import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.storage.PaymentAddressList; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; import org.whispersystems.textsecuregcm.storage.UsernamesManager; import org.whispersystems.textsecuregcm.util.Constants; @@ -514,6 +515,16 @@ public class AccountController { return Response.ok().build(); } + @Timed + @PUT + @Path("/payments") + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public void setPayments(@Auth Account account, @Valid PaymentAddressList payments) { + account.setPayments(payments.getPayments()); + accounts.update(account); + } + private CaptchaRequirement requiresCaptcha(String number, String transport, String forwardedFor, String requester, Optional captchaToken, @@ -608,6 +619,7 @@ public class AccountController { setAccountRegistrationLockFromAttributes(account, accountAttributes); account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey()); account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess()); + account.setPayments(accountAttributes.getPayments()); if (accounts.create(account)) { newUserMeter.mark(); 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 fc517aaa4..5469475a5 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java @@ -200,7 +200,9 @@ public class ProfileController { accountProfile.get().isUnrestrictedUnidentifiedAccess(), new UserCapabilities(accountProfile.get().isUuidAddressingSupported(), accountProfile.get().isGroupsV2Supported()), username.orElse(null), - null, credential.orElse(null))); + null, + credential.orElse(null), + accountProfile.get().getPayments())); } catch (InvalidInputException e) { logger.info("Bad profile request", e); throw new WebApplicationException(Response.Status.BAD_REQUEST); @@ -236,7 +238,9 @@ public class ProfileController { accountProfile.get().isUnrestrictedUnidentifiedAccess(), new UserCapabilities(accountProfile.get().isUuidAddressingSupported(), accountProfile.get().isGroupsV2Supported()), username, - accountProfile.get().getUuid(), null); + accountProfile.get().getUuid(), + null, + accountProfile.get().getPayments()); } private Optional getProfileCredential(Optional encodedProfileCredentialRequest, @@ -307,7 +311,9 @@ public class ProfileController { accountProfile.get().isUnrestrictedUnidentifiedAccess(), new UserCapabilities(accountProfile.get().isUuidAddressingSupported(), accountProfile.get().isGroupsV2Supported()), username.orElse(null), - null, null); + null, + null, + accountProfile.get().getPayments()); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java index 99bf21efb..9b47a00cc 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java @@ -21,6 +21,9 @@ import com.google.common.annotations.VisibleForTesting; import org.hibernate.validator.constraints.Length; import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; +import org.whispersystems.textsecuregcm.storage.PaymentAddress; + +import java.util.List; public class AccountAttributes { @@ -49,6 +52,9 @@ public class AccountAttributes { @JsonProperty private boolean unrestrictedUnidentifiedAccess; + @JsonProperty + private List payments; + @JsonProperty private DeviceCapabilities capabilities; @@ -56,17 +62,18 @@ public class AccountAttributes { @VisibleForTesting public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String pin) { - this(signalingKey, fetchesMessages, registrationId, null, pin, null); + this(signalingKey, fetchesMessages, registrationId, null, pin, null, null); } @VisibleForTesting - public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, String pin, String registrationLock) { + public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, String pin, String registrationLock, List payments) { this.signalingKey = signalingKey; this.fetchesMessages = fetchesMessages; this.registrationId = registrationId; this.name = name; this.pin = pin; this.registrationLock = registrationLock; + this.payments = payments; } public String getSignalingKey() { @@ -104,4 +111,8 @@ public class AccountAttributes { public DeviceCapabilities getCapabilities() { return capabilities; } + + public List getPayments() { + return payments; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/Profile.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/Profile.java index cf6acdf31..2147d25c6 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/Profile.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/Profile.java @@ -6,7 +6,9 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.annotations.VisibleForTesting; import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse; +import org.whispersystems.textsecuregcm.storage.PaymentAddress; +import java.util.List; import java.util.UUID; public class Profile { @@ -35,6 +37,9 @@ public class Profile { @JsonProperty private UUID uuid; + @JsonProperty + private List payments; + @JsonProperty @JsonSerialize(using = ProfileKeyCredentialResponseAdapter.Serializing.class) @JsonDeserialize(using = ProfileKeyCredentialResponseAdapter.Deserializing.class) @@ -45,7 +50,8 @@ public class Profile { public Profile(String name, String avatar, String identityKey, String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess, UserCapabilities capabilities, String username, UUID uuid, - ProfileKeyCredentialResponse credential) + ProfileKeyCredentialResponse credential, + List payments) { this.name = name; this.avatar = avatar; @@ -55,6 +61,7 @@ public class Profile { this.capabilities = capabilities; this.username = username; this.uuid = uuid; + this.payments = payments; this.credential = credential; } @@ -97,4 +104,9 @@ public class Profile { public UUID getUuid() { return uuid; } + + @VisibleForTesting + public List getPayments() { + return payments; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index 371991810..d0a31eae6 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -26,6 +26,7 @@ import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock; import javax.security.auth.Subject; import java.security.Principal; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -52,10 +53,10 @@ public class Account implements Principal { private String avatar; @JsonProperty - private String avatarDigest; + private String pin; @JsonProperty - private String pin; + private List payments; @JsonProperty private String registrationLock; @@ -224,14 +225,6 @@ public class Account implements Principal { this.avatar = avatar; } - public String getAvatarDigest() { - return avatarDigest; - } - - public void setAvatarDigest(String avatarDigest) { - this.avatarDigest = avatarDigest; - } - public void setPin(String pin) { this.pin = pin; } @@ -261,6 +254,14 @@ public class Account implements Principal { this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess; } + public List getPayments() { + return payments; + } + + public void setPayments(List payments) { + this.payments = payments; + } + public boolean isFor(AmbiguousIdentifier identifier) { if (identifier.hasUuid()) return identifier.getUuid().equals(uuid); else if (identifier.hasNumber()) return identifier.getNumber().equals(number); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/PaymentAddress.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/PaymentAddress.java new file mode 100644 index 000000000..8a646f703 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/PaymentAddress.java @@ -0,0 +1,51 @@ +package org.whispersystems.textsecuregcm.storage; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.annotation.RegEx; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.util.Objects; + +public class PaymentAddress { + + @JsonProperty + @NotEmpty + @Size(max = 256) + private String address; + + @JsonProperty + @NotEmpty + @Size(min = 88, max = 88) + private String signature; + + public PaymentAddress() {} + + public PaymentAddress(String address, String signature) { + this.address = address; + this.signature = signature; + } + + public String getSignature() { + return signature; + } + + public String getAddress() { + return address; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PaymentAddress that = (PaymentAddress) o; + return Objects.equals(address, that.address) && Objects.equals(signature, that.signature); + } + + @Override + public int hashCode() { + return Objects.hash(address, signature); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/PaymentAddressList.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/PaymentAddressList.java new file mode 100644 index 000000000..abf983ee5 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/PaymentAddressList.java @@ -0,0 +1,27 @@ +package org.whispersystems.textsecuregcm.storage; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +public class PaymentAddressList { + + @JsonProperty + @NotNull + @Valid + private List payments; + + public PaymentAddressList() { + + } + + public PaymentAddressList(List payments) { + this.payments = payments; + } + + public List getPayments() { + return payments; + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java index 0620779e6..13c684bb0 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java @@ -40,6 +40,8 @@ import org.whispersystems.textsecuregcm.storage.AbusiveHostRules; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.MessagesManager; +import org.whispersystems.textsecuregcm.storage.PaymentAddress; +import org.whispersystems.textsecuregcm.storage.PaymentAddressList; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; import org.whispersystems.textsecuregcm.storage.UsernamesManager; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; @@ -53,6 +55,7 @@ import java.io.IOException; import java.security.SecureRandom; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -577,7 +580,7 @@ public class AccountControllerTest { .target(String.format("/v1/accounts/code/%s", "666666")) .request() .header("Authorization", AuthHelper.getAuthHeader(SENDER_REG_LOCK, "bar")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null, null, Hex.toStringCondensed(registration_lock_key)), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null, null, Hex.toStringCondensed(registration_lock_key), null), MediaType.APPLICATION_JSON_TYPE), AccountCreationResult.class); assertThat(result.getUuid()).isNotNull(); @@ -593,7 +596,7 @@ public class AccountControllerTest { .target(String.format("/v1/accounts/code/%s", "666666")) .request() .header("Authorization", AuthHelper.getAuthHeader(SENDER_REG_LOCK, "bar")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null, null, Hex.toStringCondensed(registration_lock_key)), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null, null, Hex.toStringCondensed(registration_lock_key), null), MediaType.APPLICATION_JSON_TYPE), AccountCreationResult.class); assertThat(result.getUuid()).isNotNull(); @@ -627,7 +630,7 @@ public class AccountControllerTest { .target(String.format("/v1/accounts/code/%s", "666666")) .request() .header("Authorization", AuthHelper.getAuthHeader(SENDER_REG_LOCK, "bar")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null, null, null), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null, null, null, null), MediaType.APPLICATION_JSON_TYPE), AccountCreationResult.class); assertThat(result.getUuid()).isNotNull(); @@ -783,6 +786,78 @@ public class AccountControllerTest { assertThat(pinCapture.getValue().length()).isEqualTo(40); } + @Test + public void testSetPayments() { + PaymentAddress paymentAddress = new PaymentAddress("some address", "V15Pf5JsFcQF6AtlM3vo3OhGEgFwTh8G3iDDvShpr8QzoJmFQ+a2xb3PoXRmGF60DLq1RR2o8Fgw+f953mKvNA=="); + + Response response = + resources.getJerseyTest() + .target("/v1/accounts/payments/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID.toString(), AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new PaymentAddressList(List.of(paymentAddress)))); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(AuthHelper.VALID_ACCOUNT, times(1)).setPayments(eq(List.of(paymentAddress))); + } + + @Test + public void testSetPaymentsUnauthorized() { + PaymentAddress paymentAddress = new PaymentAddress("an address", "V15Pf5JsFcQF6AtlM3vo3OhGEgFwTh8G3iDDvShpr8QzoJmFQ+a2xb3PoXRmGF60DLq1RR2o8Fgw+f953mKvNA=="); + + Response response = + resources.getJerseyTest() + .target("/v1/accounts/payments/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.INVALID_UUID.toString(), AuthHelper.INVALID_PASSWORD)) + .put(Entity.json(new PaymentAddressList(List.of(paymentAddress)))); + + assertThat(response.getStatus()).isEqualTo(401); + } + + @Test + public void testSetPaymentsInvalidSignature() { + PaymentAddress paymentAddress = new PaymentAddress("some address", "123456789012345678901234567890123"); + + Response response = + resources.getJerseyTest() + .target("/v1/accounts/payments/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID.toString(), AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new PaymentAddressList(List.of(paymentAddress)))); + + assertThat(response.getStatus()).isEqualTo(422); + } + + @Test + public void testSetPaymentsEmptyAddress() { + PaymentAddress paymentAddress = new PaymentAddress(null, "V15Pf5JsFcQF6AtlM3vo3OhGEgFwTh8G3iDDvShpr8QzoJmFQ+a2xb3PoXRmGF60DLq1RR2o8Fgw+f953mKvNA=="); + + Response response = + resources.getJerseyTest() + .target("/v1/accounts/payments/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID.toString(), AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new PaymentAddressList(List.of(paymentAddress)))); + + assertThat(response.getStatus()).isEqualTo(422); + } + + @Test + public void testSetPaymentsEmptySignature() { + PaymentAddress paymentAddress = new PaymentAddress("some address", null); + + Response response = + resources.getJerseyTest() + .target("/v1/accounts/payments/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID.toString(), AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new PaymentAddressList(List.of(paymentAddress)))); + + assertThat(response.getStatus()).isEqualTo(422); + } + @Test public void testSetPinUnauthorized() throws Exception { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java index e74b3682e..68148f621 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java @@ -212,7 +212,7 @@ public class DeviceControllerTest { .target("/v1/devices/5678901") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters it's so long that it's even longer than 204 characters. that's a lot of characters. we're talking lots and lots and lots of characters. 12345678", null, null), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters it's so long that it's even longer than 204 characters. that's a lot of characters. we're talking lots and lots and lots of characters. 12345678", null, null, null), MediaType.APPLICATION_JSON_TYPE)); assertEquals(response.getStatus(), 422); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java index eac17a895..ca8e55876 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/ProfileControllerTest.java @@ -28,6 +28,7 @@ import org.whispersystems.textsecuregcm.s3.PolicySigner; import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.PaymentAddress; import org.whispersystems.textsecuregcm.storage.ProfilesManager; import org.whispersystems.textsecuregcm.storage.UsernamesManager; import org.whispersystems.textsecuregcm.storage.VersionedProfile; @@ -37,6 +38,7 @@ import org.whispersystems.textsecuregcm.util.SystemMapper; import javax.ws.rs.client.Entity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.List; import java.util.Optional; import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; @@ -87,17 +89,16 @@ public class ProfileControllerTest { when(profileAccount.getIdentityKey()).thenReturn("bar"); when(profileAccount.getProfileName()).thenReturn("baz"); when(profileAccount.getAvatar()).thenReturn("profiles/bang"); - when(profileAccount.getAvatarDigest()).thenReturn("buh"); when(profileAccount.getUuid()).thenReturn(AuthHelper.VALID_UUID_TWO); when(profileAccount.isEnabled()).thenReturn(true); when(profileAccount.isUuidAddressingSupported()).thenReturn(false); + when(profileAccount.getPayments()).thenReturn(List.of(new PaymentAddress("mc", "12345678901234567890123456789012"))); Account capabilitiesAccount = mock(Account.class); when(capabilitiesAccount.getIdentityKey()).thenReturn("barz"); when(capabilitiesAccount.getProfileName()).thenReturn("bazz"); when(capabilitiesAccount.getAvatar()).thenReturn("profiles/bangz"); - when(capabilitiesAccount.getAvatarDigest()).thenReturn("buz"); when(capabilitiesAccount.isEnabled()).thenReturn(true); when(capabilitiesAccount.isUuidAddressingSupported()).thenReturn(true); @@ -133,6 +134,7 @@ public class ProfileControllerTest { assertThat(profile.getName()).isEqualTo("baz"); assertThat(profile.getAvatar()).isEqualTo("profiles/bang"); assertThat(profile.getUsername()).isEqualTo("n00bkiller"); + assertThat(profile.getPayments()).isEqualTo(List.of(new PaymentAddress("mc", "12345678901234567890123456789012"))); verify(accountsManager, times(1)).get(argThat((ArgumentMatcher) identifier -> identifier != null && identifier.hasUuid() && identifier.getUuid().equals(AuthHelper.VALID_UUID_TWO))); verify(usernamesManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO)); @@ -150,6 +152,7 @@ public class ProfileControllerTest { assertThat(profile.getIdentityKey()).isEqualTo("bar"); assertThat(profile.getName()).isEqualTo("baz"); assertThat(profile.getAvatar()).isEqualTo("profiles/bang"); + assertThat(profile.getPayments()).isEqualTo(List.of(new PaymentAddress("mc", "12345678901234567890123456789012"))); assertThat(profile.getCapabilities().isUuid()).isFalse(); assertThat(profile.getUsername()).isNull(); assertThat(profile.getUuid()).isNull();; @@ -171,6 +174,7 @@ public class ProfileControllerTest { assertThat(profile.getName()).isEqualTo("baz"); assertThat(profile.getAvatar()).isEqualTo("profiles/bang"); assertThat(profile.getUsername()).isEqualTo("n00bkiller"); + assertThat(profile.getPayments()).isEqualTo(List.of(new PaymentAddress("mc", "12345678901234567890123456789012"))); assertThat(profile.getUuid()).isEqualTo(AuthHelper.VALID_UUID_TWO); verify(accountsManager, times(1)).get(eq(AuthHelper.VALID_UUID_TWO));