diff --git a/.gitignore b/.gitignore index bed011ea8..1671169f0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ run.sh local.yml config/production.yml config/federated.yml +config/staging.yml +.opsmanage diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java index 8998a8bef..11fb384ad 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java @@ -70,7 +70,14 @@ public class KeysController { @PUT @Consumes(MediaType.APPLICATION_JSON) public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) { - Device device = account.getAuthenticatedDevice().get(); + Device device = account.getAuthenticatedDevice().get(); + String identityKey = preKeys.getLastResortKey().getIdentityKey(); + + if (!identityKey.equals(account.getIdentityKey())) { + account.setIdentityKey(identityKey); + accounts.update(account); + } + keys.store(account.getNumber(), device.getId(), preKeys.getKeys(), preKeys.getLastResortKey()); } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyList.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyList.java index 49e0ea7cd..df2c1d7f5 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyList.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyList.java @@ -17,6 +17,7 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; import org.hibernate.validator.constraints.NotEmpty; import javax.validation.Valid; @@ -27,6 +28,7 @@ public class PreKeyList { @JsonProperty @NotNull + @Valid private PreKey lastResortKey; @JsonProperty @@ -38,7 +40,17 @@ public class PreKeyList { return keys; } + @VisibleForTesting + public void setKeys(List keys) { + this.keys = keys; + } + public PreKey getLastResortKey() { return lastResortKey; } + + @VisibleForTesting + public void setLastResortKey(PreKey lastResortKey) { + this.lastResortKey = lastResortKey; + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index ab2971d97..33a42c1a6 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -28,7 +28,7 @@ import java.util.List; public class Account implements Serializable { - public static final int MEMCACHE_VERION = 2; + public static final int MEMCACHE_VERION = 3; @JsonIgnore private long id; @@ -42,16 +42,14 @@ public class Account implements Serializable { @JsonProperty private List devices = new LinkedList<>(); + @JsonProperty + private String identityKey; + @JsonIgnore private Optional authenticatedDevice; public Account() {} - public Account(String number, boolean supportsSms) { - this.number = number; - this.supportsSms = supportsSms; - } - @VisibleForTesting public Account(String number, boolean supportsSms, List devices) { this.number = number; @@ -142,4 +140,12 @@ public class Account implements Serializable { public Optional getRelay() { return Optional.absent(); } + + public void setIdentityKey(String identityKey) { + this.identityKey = identityKey; + } + + public String getIdentityKey() { + return identityKey; + } } diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java index 0ec02e555..c3fe4b3b4 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java @@ -4,8 +4,10 @@ import com.google.common.base.Optional; import com.sun.jersey.api.client.ClientResponse; import com.yammer.dropwizard.testing.ResourceTest; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.whispersystems.textsecuregcm.controllers.KeysController; import org.whispersystems.textsecuregcm.entities.PreKey; +import org.whispersystems.textsecuregcm.entities.PreKeyList; import org.whispersystems.textsecuregcm.entities.PreKeyStatus; import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; import org.whispersystems.textsecuregcm.limits.RateLimiter; @@ -16,6 +18,7 @@ import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Keys; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import javax.ws.rs.core.MediaType; import java.util.LinkedList; import java.util.List; @@ -30,11 +33,13 @@ public class KeyControllerTest extends ResourceTest { private final int SAMPLE_REGISTRATION_ID = 999; private final int SAMPLE_REGISTRATION_ID2 = 1002; - private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, Device.MASTER_ID, 1234, "test1", "test2", false); - private final PreKey SAMPLE_KEY2 = new PreKey(2, EXISTS_NUMBER, 2, 5667, "test3", "test4", false ); - private final PreKey SAMPLE_KEY3 = new PreKey(3, EXISTS_NUMBER, 3, 334, "test5", "test6", false); - private final Keys keys = mock(Keys.class ); - private final AccountsManager accounts = mock(AccountsManager.class); + private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, Device.MASTER_ID, 1234, "test1", "test2", false); + private final PreKey SAMPLE_KEY2 = new PreKey(2, EXISTS_NUMBER, 2, 5667, "test3", "test4", false ); + private final PreKey SAMPLE_KEY3 = new PreKey(3, EXISTS_NUMBER, 3, 334, "test5", "test6", false ); + private final Keys keys = mock(Keys.class ); + private final AccountsManager accounts = mock(AccountsManager.class); + private final Account existsAccount = mock(Account.class ); + @Override protected void setUpResources() { @@ -46,7 +51,6 @@ public class KeyControllerTest extends ResourceTest { Device sampleDevice = mock(Device.class ); Device sampleDevice2 = mock(Device.class); Device sampleDevice3 = mock(Device.class); - Account existsAccount = mock(Account.class); when(sampleDevice.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID); when(sampleDevice2.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2); @@ -76,6 +80,8 @@ public class KeyControllerTest extends ResourceTest { when(keys.get(EXISTS_NUMBER)).thenReturn(Optional.of(new UnstructuredPreKeyList(allKeys))); when(keys.getCount(eq(AuthHelper.VALID_NUMBER), eq(1L))).thenReturn(5); + when(AuthHelper.VALID_ACCOUNT.getIdentityKey()).thenReturn(null); + addResource(new KeysController(rateLimiters, keys, accounts, null)); } @@ -165,4 +171,42 @@ public class KeyControllerTest extends ResourceTest { assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(401); } + @Test + public void putKeysTest() throws Exception { + final PreKey newKey = new PreKey(0, null, 1L, 31337, "foobar", "foobarbaz", false); + final PreKey lastResortKey = new PreKey(0, null, 1L, 0xFFFFFF, "fooz", "foobarbaz", false); + + List preKeys = new LinkedList() {{ + add(newKey); + }}; + + PreKeyList preKeyList = new PreKeyList(); + preKeyList.setKeys(preKeys); + preKeyList.setLastResortKey(lastResortKey); + + ClientResponse response = + client().resource("/v1/keys") + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .type(MediaType.APPLICATION_JSON_TYPE) + .put(ClientResponse.class, preKeyList); + + assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(204); + + ArgumentCaptor listCaptor = ArgumentCaptor.forClass(List.class ); + ArgumentCaptor lastResortCaptor = ArgumentCaptor.forClass(PreKey.class); + verify(keys).store(eq(AuthHelper.VALID_NUMBER), eq(1L), listCaptor.capture(), lastResortCaptor.capture()); + + List capturedList = listCaptor.getValue(); + assertThat(capturedList.size() == 1); + assertThat(capturedList.get(0).getIdentityKey().equals("foobarbaz")); + assertThat(capturedList.get(0).getKeyId() == 31337); + assertThat(capturedList.get(0).getPublicKey().equals("foobar")); + + assertThat(lastResortCaptor.getValue().getPublicKey().equals("fooz")); + assertThat(lastResortCaptor.getValue().getIdentityKey().equals("foobarbaz")); + + verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq("foobarbaz")); + verify(accounts).update(AuthHelper.VALID_ACCOUNT); + } + } \ No newline at end of file diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java b/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java index ba34e9854..6f99fe902 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java @@ -27,20 +27,20 @@ public class AuthHelper { public static final String INVVALID_NUMBER = "+14151111111"; public static final String INVALID_PASSWORD = "bar"; - public static MultiBasicAuthProvider getAuthenticator() { - AccountsManager accounts = mock(AccountsManager.class ); - Account account = mock(Account.class ); - Device device = mock(Device.class ); - AuthenticationCredentials credentials = mock(AuthenticationCredentials.class); + public static AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class ); + public static Account VALID_ACCOUNT = mock(Account.class ); + public static Device VALID_DEVICE = mock(Device.class ); + public static AuthenticationCredentials VALID_CREDENTIALS = mock(AuthenticationCredentials.class); - when(credentials.verify("foo")).thenReturn(true); - when(device.getAuthenticationCredentials()).thenReturn(credentials); - when(device.getId()).thenReturn(1L); - when(account.getDevice(anyLong())).thenReturn(Optional.of(device)); - when(account.getNumber()).thenReturn(VALID_NUMBER); - when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device)); - when(account.getRelay()).thenReturn(Optional.absent()); - when(accounts.get(VALID_NUMBER)).thenReturn(Optional.of(account)); + public static MultiBasicAuthProvider getAuthenticator() { + when(VALID_CREDENTIALS.verify("foo")).thenReturn(true); + when(VALID_DEVICE.getAuthenticationCredentials()).thenReturn(VALID_CREDENTIALS); + when(VALID_DEVICE.getId()).thenReturn(1L); + when(VALID_ACCOUNT.getDevice(anyLong())).thenReturn(Optional.of(VALID_DEVICE)); + when(VALID_ACCOUNT.getNumber()).thenReturn(VALID_NUMBER); + when(VALID_ACCOUNT.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE)); + when(VALID_ACCOUNT.getRelay()).thenReturn(Optional.absent()); + when(ACCOUNTS_MANAGER.get(VALID_NUMBER)).thenReturn(Optional.of(VALID_ACCOUNT)); List peer = new LinkedList() {{ add(new FederatedPeer("cyanogen", "https://foo", "foofoo", "bazzzzz")); @@ -51,7 +51,7 @@ public class AuthHelper { return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(federationConfiguration), FederatedPeer.class, - new AccountAuthenticator(accounts), + new AccountAuthenticator(ACCOUNTS_MANAGER), Account.class, "WhisperServer"); }