Device provisioning fixes.

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-01-21 15:15:40 -08:00
parent f7132bdbbc
commit 45a0b74b89
13 changed files with 123 additions and 54 deletions

View File

@ -31,6 +31,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PendingDevicesManager;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.textsecuregcm.util.VerificationCode;
import javax.validation.Valid;
@ -119,6 +120,7 @@ public class DeviceController {
device.setSignalingKey(accountAttributes.getSignalingKey());
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setId(account.get().getNextDeviceId());
device.setLastSeen(Util.todayInMillis());
account.get().addDevice(device);
accounts.update(account.get());

View File

@ -18,7 +18,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import io.dropwizard.auth.Auth;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal;
@ -74,8 +74,8 @@ public class ReceiptController {
private void sendDirectReceipt(Account source, String destination, long messageId)
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
{
Account destinationAccount = getDestinationAccount(destination);
List<Device> destinationDevices = destinationAccount.getDevices();
Account destinationAccount = getDestinationAccount(destination);
Set<Device> destinationDevices = destinationAccount.getDevices();
OutgoingMessageSignal.Builder message =
OutgoingMessageSignal.newBuilder()

View File

@ -16,6 +16,7 @@
*/
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
@ -42,7 +43,19 @@ public class PreKeyResponseV2 {
}
@VisibleForTesting
public List<PreKeyResponseItemV2> getDevices() {
return devices;
@JsonIgnore
public PreKeyResponseItemV2 getDevice(int deviceId) {
for (PreKeyResponseItemV2 device : devices) {
if (device.getDeviceId() == deviceId) return device;
}
return null;
}
@VisibleForTesting
@JsonIgnore
public int getDevicesCount() {
return devices.size();
}
}

View File

@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
@Override
public Optional<Device> getAuthenticatedDevice() {
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0, null));
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0, null, System.currentTimeMillis()));
}
}

View File

@ -22,8 +22,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import java.util.LinkedList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
public class Account {
@ -36,7 +36,7 @@ public class Account {
private boolean supportsSms;
@JsonProperty
private List<Device> devices = new LinkedList<>();
private Set<Device> devices = new HashSet<>();
@JsonProperty
private String identityKey;
@ -47,7 +47,7 @@ public class Account {
public Account() {}
@VisibleForTesting
public Account(String number, boolean supportsSms, List<Device> devices) {
public Account(String number, boolean supportsSms, Set<Device> devices) {
this.number = number;
this.supportsSms = supportsSms;
this.devices = devices;
@ -78,14 +78,11 @@ public class Account {
}
public void addDevice(Device device) {
this.devices.remove(device);
this.devices.add(device);
}
public void setDevices(List<Device> devices) {
this.devices = devices;
}
public List<Device> getDevices() {
public Set<Device> getDevices() {
return devices;
}
@ -113,7 +110,9 @@ public class Account {
long highestDevice = Device.MASTER_ID;
for (Device device : devices) {
if (device.getId() > highestDevice) {
if (!device.isActive()) {
return device.getId();
} else if (device.getId() > highestDevice) {
highestDevice = device.getId();
}
}

View File

@ -22,6 +22,8 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.concurrent.TimeUnit;
public class Device {
public static final long MASTER_ID = 1;
@ -56,12 +58,15 @@ public class Device {
@JsonProperty
private SignedPreKey signedPreKey;
@JsonProperty
private long lastSeen;
public Device() {}
public Device(long id, String authToken, String salt,
String signalingKey, String gcmId, String apnId,
boolean fetchesMessages, int registrationId,
SignedPreKey signedPreKey)
SignedPreKey signedPreKey, long lastSeen)
{
this.id = id;
this.authToken = authToken;
@ -72,6 +77,7 @@ public class Device {
this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId;
this.signedPreKey = signedPreKey;
this.lastSeen = lastSeen;
}
public String getApnId() {
@ -86,6 +92,14 @@ public class Device {
}
}
public void setLastSeen(long lastSeen) {
this.lastSeen = lastSeen;
}
public long getLastSeen() {
return lastSeen;
}
public String getGcmId() {
return gcmId;
}
@ -124,7 +138,10 @@ public class Device {
}
public boolean isActive() {
return fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId());
boolean hasChannel = fetchesMessages || !Util.isEmpty(getApnId()) || !Util.isEmpty(getGcmId());
return (id == MASTER_ID && hasChannel) ||
(id != MASTER_ID && hasChannel && signedPreKey != null && lastSeen > (System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30)));
}
public boolean getFetchesMessages() {
@ -158,4 +175,17 @@ public class Device {
public long getPushTimestamp() {
return pushTimestamp;
}
@Override
public boolean equals(Object other) {
if (other == null || !(other instanceof Device)) return false;
Device that = (Device)other;
return this.id == that.id;
}
@Override
public int hashCode() {
return (int)this.id;
}
}

View File

@ -21,6 +21,7 @@ import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class Util {
@ -119,4 +120,8 @@ public class Util {
Thread.sleep(i);
} catch (InterruptedException ie) {}
}
public static long todayInMillis() {
return TimeUnit.DAYS.toMillis(TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()));
}
}

View File

@ -9,6 +9,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.PubSubManager;
import org.whispersystems.textsecuregcm.storage.StoredMessages;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.websocket.session.WebSocketSessionContext;
import org.whispersystems.websocket.setup.WebSocketConnectListener;
@ -48,6 +49,11 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
return;
}
if (device.get().getLastSeen() != Util.todayInMillis()) {
device.get().setLastSeen(Util.todayInMillis());
accountsManager.update(account.get());
}
final WebSocketConnection connection = new WebSocketConnection(accountsManager, pushSender,
storedMessages, pubSubManager,
account.get(), device.get(),

View File

@ -27,8 +27,10 @@ import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.core.MediaType;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.hamcrest.CoreMatchers.equalTo;
@ -69,13 +71,13 @@ public class FederatedControllerTest {
@Before
public void setup() throws Exception {
List<Device> singleDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null, System.currentTimeMillis()));
}};
List<Device> multiDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null, System.currentTimeMillis()));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null, System.currentTimeMillis()));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);

View File

@ -26,8 +26,10 @@ import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.core.MediaType;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.fest.assertions.api.Assertions.assertThat;
@ -73,7 +75,7 @@ public class KeyControllerTest {
final Device sampleDevice3 = mock(Device.class);
final Device sampleDevice4 = mock(Device.class);
List<Device> allDevices = new LinkedList<Device>() {{
Set<Device> allDevices = new HashSet<Device>() {{
add(sampleDevice);
add(sampleDevice2);
add(sampleDevice3);
@ -198,10 +200,10 @@ public class KeyControllerTest {
.get(PreKeyResponseV2.class);
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
assertThat(result.getDevices().size()).isEqualTo(1);
assertThat(result.getDevices().get(0).getPreKey().getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
assertThat(result.getDevices().get(0).getPreKey().getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
assertThat(result.getDevices().get(0).getSignedPreKey()).isEqualTo(existsAccount.getDevice(1).get().getSignedPreKey());
assertThat(result.getDevicesCount()).isEqualTo(1);
assertThat(result.getDevice(1).getPreKey().getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
assertThat(result.getDevice(1).getPreKey().getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
assertThat(result.getDevice(1).getSignedPreKey()).isEqualTo(existsAccount.getDevice(1).get().getSignedPreKey());
verify(keys).get(eq(EXISTS_NUMBER), eq(1L));
verifyNoMoreInteractions(keys);
@ -245,13 +247,13 @@ public class KeyControllerTest {
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyResponseV2.class);
assertThat(results.getDevices().size()).isEqualTo(3);
assertThat(results.getDevicesCount()).isEqualTo(3);
assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
PreKeyV2 signedPreKey = results.getDevices().get(0).getSignedPreKey();
PreKeyV2 preKey = results.getDevices().get(0).getPreKey();
long registrationId = results.getDevices().get(0).getRegistrationId();
long deviceId = results.getDevices().get(0).getDeviceId();
PreKeyV2 signedPreKey = results.getDevice(1).getSignedPreKey();
PreKeyV2 preKey = results.getDevice(1).getPreKey();
long registrationId = results.getDevice(1).getRegistrationId();
long deviceId = results.getDevice(1).getDeviceId();
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
@ -260,10 +262,10 @@ public class KeyControllerTest {
assertThat(signedPreKey.getPublicKey()).isEqualTo(SAMPLE_SIGNED_KEY.getPublicKey());
assertThat(deviceId).isEqualTo(1);
signedPreKey = results.getDevices().get(1).getSignedPreKey();
preKey = results.getDevices().get(1).getPreKey();
registrationId = results.getDevices().get(1).getRegistrationId();
deviceId = results.getDevices().get(1).getDeviceId();
signedPreKey = results.getDevice(2).getSignedPreKey();
preKey = results.getDevice(2).getPreKey();
registrationId = results.getDevice(2).getRegistrationId();
deviceId = results.getDevice(2).getDeviceId();
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId());
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey());
@ -272,10 +274,10 @@ public class KeyControllerTest {
assertThat(signedPreKey.getPublicKey()).isEqualTo(SAMPLE_SIGNED_KEY2.getPublicKey());
assertThat(deviceId).isEqualTo(2);
signedPreKey = results.getDevices().get(2).getSignedPreKey();
preKey = results.getDevices().get(2).getPreKey();
registrationId = results.getDevices().get(2).getRegistrationId();
deviceId = results.getDevices().get(2).getDeviceId();
signedPreKey = results.getDevice(4).getSignedPreKey();
preKey = results.getDevice(4).getPreKey();
registrationId = results.getDevice(4).getRegistrationId();
deviceId = results.getDevice(4).getDeviceId();
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY4.getKeyId());
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY4.getPublicKey());

View File

@ -6,10 +6,13 @@ import com.sun.jersey.api.client.ClientResponse;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.MismatchedDevices;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
@ -21,8 +24,9 @@ import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.core.MediaType;
import java.util.LinkedList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.hamcrest.CoreMatchers.equalTo;
@ -57,13 +61,14 @@ public class MessageControllerTest {
@Before
public void setup() throws Exception {
List<Device> singleDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null, System.currentTimeMillis()));
}};
List<Device> multiDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis()));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis()));
add(new Device(3, "foo", "bar", "baz", "isgcm", null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31)));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);

View File

@ -18,8 +18,11 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.fest.assertions.api.Assertions.assertThat;
@ -47,13 +50,13 @@ public class ReceiptControllerTest {
@Before
public void setup() throws Exception {
List<Device> singleDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null, System.currentTimeMillis()));
}};
List<Device> multiDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null, System.currentTimeMillis()));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null, System.currentTimeMillis()));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);

View File

@ -25,8 +25,10 @@ import org.whispersystems.websocket.session.WebSocketSessionContext;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import io.dropwizard.auth.basic.BasicCredentials;
import static org.junit.Assert.assertTrue;
@ -127,7 +129,7 @@ public class WebSocketConnectionTest {
final Device sender1device = mock(Device.class);
List<Device> sender1devices = new LinkedList<Device>() {{
Set<Device> sender1devices = new HashSet<Device>() {{
add(sender1device);
}};