Transparent data controller

This commit is contained in:
Moxie Marlinspike 2018-12-06 11:59:43 -08:00 committed by Brian Acton
parent ea38645493
commit 6ce686ab9c
12 changed files with 251 additions and 6 deletions

View File

@ -141,6 +141,8 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private VoiceVerificationConfiguration voiceVerification;
private Map<String, String> transparentDataIndex = new HashMap<>();
public VoiceVerificationConfiguration getVoiceVerificationConfiguration() {
return voiceVerification;
@ -244,4 +246,8 @@ public class WhisperServerConfiguration extends Configuration {
return results;
}
public Map<String, String> getTransparentDataIndex() {
return transparentDataIndex;
}
}

View File

@ -39,6 +39,7 @@ import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.controllers.ProfileController;
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
import org.whispersystems.textsecuregcm.controllers.VoiceVerificationController;
import org.whispersystems.textsecuregcm.controllers.TransparentDataController;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
@ -225,6 +226,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(new ProvisioningController(rateLimiters, pushSender));
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays())));
environment.jersey().register(new VoiceVerificationController(config.getVoiceVerificationConfiguration().getUrl(), config.getVoiceVerificationConfiguration().getLocales()));
environment.jersey().register(new TransparentDataController(accountsManager, config.getTransparentDataIndex()));
environment.jersey().register(attachmentController);
environment.jersey().register(keysController);
environment.jersey().register(messageController);

View File

@ -0,0 +1,42 @@
package org.whispersystems.textsecuregcm.controllers;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.PublicAccount;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Map;
import java.util.Optional;
@Path("/v1/transparency/")
public class TransparentDataController {
private final AccountsManager accountsManager;
private final Map<String, String> transparentDataIndex;
public TransparentDataController(AccountsManager accountsManager,
Map<String, String> transparentDataIndex)
{
this.accountsManager = accountsManager;
this.transparentDataIndex = transparentDataIndex;
}
@GET
@Path("/account/{id}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PublicAccount> getAccount(@PathParam("id") String id) {
String index = transparentDataIndex.get(id);
if (index != null) {
return accountsManager.get(index).map(PublicAccount::new);
}
return Optional.empty();
}
}

View File

@ -30,9 +30,9 @@ import java.util.concurrent.TimeUnit;
public class Account implements Principal {
public static final int MEMCACHE_VERION = 5;
static final int MEMCACHE_VERION = 5;
@JsonProperty
@JsonIgnore
private String number;
@JsonProperty

View File

@ -111,7 +111,7 @@ public abstract class Accounts {
{
try {
Account account = mapper.readValue(resultSet.getString(DATA), Account.class);
// account.setId(resultSet.getLong(ID));
account.setNumber(resultSet.getString(NUMBER));
return account;
} catch (IOException e) {

View File

@ -136,8 +136,14 @@ public class AccountsManager {
{
String json = jedis.get(getKey(number));
if (json != null) return Optional.of(mapper.readValue(json, Account.class));
else return Optional.empty();
if (json != null) {
Account account = mapper.readValue(json, Account.class);
account.setNumber(number);
return Optional.of(account);
}
return Optional.empty();
} catch (IOException e) {
logger.warn("AccountsManager", "Deserialization error", e);
return Optional.empty();

View File

@ -0,0 +1,18 @@
package org.whispersystems.textsecuregcm.storage;
public class PublicAccount extends Account {
public PublicAccount() {}
public PublicAccount(Account account) {
setIdentityKey(account.getIdentityKey());
setUnidentifiedAccessKey(account.getUnidentifiedAccessKey().orElse(null));
setUnrestrictedUnidentifiedAccess(account.isUnrestrictedUnidentifiedAccess());
setAvatar(account.getAvatar());
setProfileName(account.getProfileName());
setPin("******");
account.getDevices().forEach(this::addDevice);
}
}

View File

@ -0,0 +1,129 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.hamcrest.MatcherAssert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.textsecuregcm.controllers.TransparentDataController;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PublicAccount;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static junit.framework.TestCase.*;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.asJson;
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.jsonFixture;
public class TransparentDataControllerTest {
private final AccountsManager accountsManager = mock(AccountsManager.class);
private final Map<String, String> indexMap = new HashMap<>();
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
.addProvider(new RateLimitExceededExceptionMapper())
.setMapper(SystemMapper.getMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new TransparentDataController(accountsManager, indexMap))
.build();
@Before
public void setup() {
Account accountOne = new Account("+14151231111", Collections.singleton(new Device(1, "foo", "bar", "salt", "keykey", "gcm-id", "apn-id", "voipapn-id", true, 1234, new SignedPreKey(5, "public-signed", "signtture-signed"), 31337, 31336, "CoolClient", true)), new byte[16]);
Account accountTwo = new Account("+14151232222", Collections.singleton(new Device(1, "2foo", "2bar", "2salt", "2keykey", "2gcm-id", "2apn-id", "2voipapn-id", true, 1234, new SignedPreKey(5, "public-signed", "signtture-signed"), 31337, 31336, "CoolClient", true)), new byte[16]);
accountOne.setProfileName("OneProfileName");
accountOne.setIdentityKey("identity_key_value");
accountTwo.setProfileName("TwoProfileName");
accountTwo.setIdentityKey("different_identity_key_value");
indexMap.put("1", "+14151231111");
indexMap.put("2", "+14151232222");
when(accountsManager.get(eq("+14151231111"))).thenReturn(Optional.of(accountOne));
when(accountsManager.get(eq("+14151232222"))).thenReturn(Optional.of(accountTwo));
}
@Test
public void testAccountOne() throws IOException {
Response response = resources.getJerseyTest()
.target(String.format("/v1/transparency/account/%s", "1"))
.request()
.get();
assertEquals(200, response.getStatus());
Account result = response.readEntity(PublicAccount.class);
assertTrue(result.getPin().isPresent());
assertEquals("******", result.getPin().get());
assertNull(result.getNumber());
assertEquals("OneProfileName", result.getProfileName());
assertThat("Account serialization works",
asJson(result),
is(equalTo(jsonFixture("fixtures/transparent_account.json"))));
verify(accountsManager, times(1)).get(eq("+14151231111"));
verifyNoMoreInteractions(accountsManager);
}
@Test
public void testAccountTwo() throws IOException {
Response response = resources.getJerseyTest()
.target(String.format("/v1/transparency/account/%s", "2"))
.request()
.get();
assertEquals(200, response.getStatus());
Account result = response.readEntity(PublicAccount.class);
assertTrue(result.getPin().isPresent());
assertEquals("******", result.getPin().get());
assertNull(result.getNumber());
assertEquals("TwoProfileName", result.getProfileName());
assertThat("Account serialization works 2",
asJson(result),
is(equalTo(jsonFixture("fixtures/transparent_account2.json"))));
verify(accountsManager, times(1)).get(eq("+14151232222"));
}
@Test
public void testAccountMissing() {
Response response = resources.getJerseyTest()
.target(String.format("/v1/transparency/account/%s", "3"))
.request()
.get();
assertEquals(404, response.getStatus());
verifyNoMoreInteractions(accountsManager);
}
}

View File

@ -0,0 +1,38 @@
package org.whispersystems.textsecuregcm.tests.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PublicAccount;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertNull;
public class PublicAccountTest {
@Test
public void testPinSanitation() throws IOException {
Set<Device> devices = Collections.singleton(new Device(1, "foo", "bar", "12345", null, "gcm-1234", null, null, true, 1234, new SignedPreKey(1, "public-foo", "signature-foo"), 31337, 31336, "Android4Life", true));
Account account = new Account("+14151231234", devices, new byte[16]);
account.setPin("123456");
PublicAccount publicAccount = new PublicAccount(account);
String serialized = SystemMapper.getMapper().writeValueAsString(publicAccount);
JsonNode result = SystemMapper.getMapper().readTree(serialized);
assertEquals("******", result.get("pin").textValue());
assertNull(result.get("number"));
}
}

View File

@ -5,13 +5,15 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.io.IOException;
import static io.dropwizard.testing.FixtureHelpers.fixture;
public class JsonHelpers {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ObjectMapper objectMapper = SystemMapper.getMapper();
public static String asJson(Object object) throws JsonProcessingException {
return objectMapper.writeValueAsString(object);

View File

@ -0,0 +1 @@
{"devices":[{"id":1,"name":"foo","authToken":"bar","salt":"salt","signalingKey":"keykey","gcmId":"gcm-id","apnId":"apn-id","voipApnId":"voipapn-id","pushTimestamp":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"identity_key_value","name":"OneProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}

View File

@ -0,0 +1 @@
{"devices":[{"id":1,"name":"2foo","authToken":"2bar","salt":"2salt","signalingKey":"2keykey","gcmId":"2gcm-id","apnId":"2apn-id","voipApnId":"2voipapn-id","pushTimestamp":0,"fetchesMessages":true,"registrationId":1234,"signedPreKey":{"keyId":5,"publicKey":"public-signed","signature":"signtture-signed"},"lastSeen":31337,"created":31336,"userAgent":"CoolClient","unauthenticatedDelivery":true}],"identityKey":"different_identity_key_value","name":"TwoProfileName","avatar":null,"avatarDigest":null,"pin":"******","uak":"AAAAAAAAAAAAAAAAAAAAAA==","uua":false}