Support for advertising payment addresses on profile

This commit is contained in:
Moxie Marlinspike 2020-04-02 12:45:24 -07:00
parent 3432529f9c
commit 95f0ce1816
10 changed files with 221 additions and 22 deletions

View File

@ -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<String> 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();

View File

@ -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<ProfileKeyCredentialResponse> getProfileCredential(Optional<String> 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());
}

View File

@ -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<PaymentAddress> 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<PaymentAddress> 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<PaymentAddress> getPayments() {
return payments;
}
}

View File

@ -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<PaymentAddress> 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<PaymentAddress> 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<PaymentAddress> getPayments() {
return payments;
}
}

View File

@ -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<PaymentAddress> 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<PaymentAddress> getPayments() {
return payments;
}
public void setPayments(List<PaymentAddress> 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);

View File

@ -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);
}
}

View File

@ -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<PaymentAddress> payments;
public PaymentAddressList() {
}
public PaymentAddressList(List<PaymentAddress> payments) {
this.payments = payments;
}
public List<PaymentAddress> getPayments() {
return payments;
}
}

View File

@ -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 {

View File

@ -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);

View File

@ -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<AmbiguousIdentifier>) 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));