From f7132bdbbc9e494c4a5527234605e8b022716aef Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Wed, 21 Jan 2015 13:56:58 -0800 Subject: [PATCH] Rearrange provisioning flow. Add needsMessageSync response. // FREEBIE --- .../textsecuregcm/WhisperServerService.java | 6 +- .../controllers/DeviceController.java | 5 +- .../InvalidDestinationException.java | 7 +++ .../controllers/MessageController.java | 61 ++++++++++--------- .../controllers/ProvisioningController.java | 53 ++++++++++++++++ .../entities/ProvisioningMessage.java | 8 +-- .../entities/SendMessageResponse.java | 16 +++++ .../textsecuregcm/storage/Account.java | 10 +++ .../websocket/ProvisioningAddress.java | 10 +-- .../controllers/DeviceControllerTest.java | 2 +- .../controllers/MessageControllerTest.java | 4 +- 11 files changed, 131 insertions(+), 51 deletions(-) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/controllers/InvalidDestinationException.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/entities/SendMessageResponse.java diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index ea17a8710..5e1978bfd 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -39,6 +39,7 @@ import org.whispersystems.textsecuregcm.controllers.KeepAliveController; import org.whispersystems.textsecuregcm.controllers.KeysControllerV1; import org.whispersystems.textsecuregcm.controllers.KeysControllerV2; import org.whispersystems.textsecuregcm.controllers.MessageController; +import org.whispersystems.textsecuregcm.controllers.ProvisioningController; import org.whispersystems.textsecuregcm.controllers.ReceiptController; import org.whispersystems.textsecuregcm.federation.FederatedClientManager; import org.whispersystems.textsecuregcm.federation.FederatedPeer; @@ -182,6 +183,7 @@ public class WhisperServerService extends Application storedVerificationCode = pendingDevices.getCodeForNumber(number); if (!storedVerificationCode.isPresent() || - !verificationCode.equals(storedVerificationCode.get())) + !MessageDigest.isEqual(verificationCode.getBytes(), storedVerificationCode.get().getBytes())) { throw new WebApplicationException(Response.status(403).build()); } diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/InvalidDestinationException.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/InvalidDestinationException.java new file mode 100644 index 000000000..2ae24a5c6 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/InvalidDestinationException.java @@ -0,0 +1,7 @@ +package org.whispersystems.textsecuregcm.controllers; + +public class InvalidDestinationException extends Exception { + public InvalidDestinationException(String message) { + super(message); + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java index 04ebb689e..bfe4c3562 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java @@ -26,7 +26,7 @@ import org.whispersystems.textsecuregcm.entities.IncomingMessageList; import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import org.whispersystems.textsecuregcm.entities.MessageResponse; import org.whispersystems.textsecuregcm.entities.MismatchedDevices; -import org.whispersystems.textsecuregcm.entities.ProvisioningMessage; +import org.whispersystems.textsecuregcm.entities.SendMessageResponse; import org.whispersystems.textsecuregcm.entities.StaleDevices; import org.whispersystems.textsecuregcm.federation.FederatedClient; import org.whispersystems.textsecuregcm.federation.FederatedClientManager; @@ -39,8 +39,6 @@ import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.util.Base64; -import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException; -import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress; import javax.validation.Valid; import javax.ws.rs.Consumes; @@ -85,16 +83,21 @@ public class MessageController { @Path("/{destination}") @PUT @Consumes(MediaType.APPLICATION_JSON) - public void sendMessage(@Auth Account source, - @PathParam("destination") String destinationName, - @Valid IncomingMessageList messages) + @Produces(MediaType.APPLICATION_JSON) + public SendMessageResponse sendMessage(@Auth Account source, + @PathParam("destination") String destinationName, + @Valid IncomingMessageList messages) throws IOException, RateLimitExceededException { rateLimiters.getMessagesLimiter().validate(source.getNumber()); try { - if (messages.getRelay() == null) sendLocalMessage(source, destinationName, messages); - else sendRelayMessage(source, destinationName, messages); + boolean isSyncMessage = source.getNumber().equals(destinationName); + + if (messages.getRelay() == null) sendLocalMessage(source, destinationName, messages, isSyncMessage); + else sendRelayMessage(source, destinationName, messages, isSyncMessage); + + return new SendMessageResponse(!isSyncMessage && source.getActiveDeviceCount() > 1); } catch (NoSuchUserException e) { throw new WebApplicationException(Response.status(404).build()); } catch (MismatchedDevicesException e) { @@ -108,6 +111,8 @@ public class MessageController { .type(MediaType.APPLICATION_JSON) .entity(new StaleDevices(e.getStaleDevices())) .build()); + } catch (InvalidDestinationException e) { + throw new WebApplicationException(Response.status(400).build()); } } @@ -131,29 +136,18 @@ public class MessageController { } } - @Timed - @PUT - @Path("/provisioning/{destination}") - @Consumes(MediaType.APPLICATION_JSON) - public void sendProvisioningMessage(@Auth Account source, - @PathParam("destination") String destinationName, - @Valid ProvisioningMessage message) - throws RateLimitExceededException, InvalidWebsocketAddressException, IOException - { - rateLimiters.getMessagesLimiter().validate(source.getNumber()); - - pushSender.getWebSocketSender().sendProvisioningMessage(new ProvisioningAddress(destinationName), - Base64.decode(message.getBody())); - } - private void sendLocalMessage(Account source, String destinationName, - IncomingMessageList messages) + IncomingMessageList messages, + boolean isSyncMessage) throws NoSuchUserException, MismatchedDevicesException, IOException, StaleDevicesException { - Account destination = getDestinationAccount(destinationName); + Account destination; - validateCompleteDeviceList(destination, messages.getMessages()); + if (!isSyncMessage) destination = getDestinationAccount(destinationName); + else destination = source; + + validateCompleteDeviceList(destination, messages.getMessages(), isSyncMessage); validateRegistrationIds(destination, messages.getMessages()); for (IncomingMessage incomingMessage : messages.getMessages()) { @@ -201,9 +195,12 @@ public class MessageController { private void sendRelayMessage(Account source, String destinationName, - IncomingMessageList messages) - throws IOException, NoSuchUserException + IncomingMessageList messages, + boolean isSyncMessage) + throws IOException, NoSuchUserException, InvalidDestinationException { + if (isSyncMessage) throw new InvalidDestinationException("Transcript messages can't be relayed!"); + try { FederatedClient client = federatedClientManager.getClient(messages.getRelay()); client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(), @@ -246,7 +243,9 @@ public class MessageController { } } - private void validateCompleteDeviceList(Account account, List messages) + private void validateCompleteDeviceList(Account account, + List messages, + boolean isSyncMessage) throws MismatchedDevicesException { Set messageDeviceIds = new HashSet<>(); @@ -260,7 +259,9 @@ public class MessageController { } for (Device device : account.getDevices()) { - if (device.isActive()) { + if (device.isActive() && + !(isSyncMessage && device.getId() == account.getAuthenticatedDevice().get().getId())) + { accountDeviceIds.add(device.getId()); if (!messageDeviceIds.contains(device.getId())) { diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java new file mode 100644 index 000000000..65f7ef032 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/ProvisioningController.java @@ -0,0 +1,53 @@ +package org.whispersystems.textsecuregcm.controllers; + +import com.codahale.metrics.annotation.Timed; +import org.whispersystems.textsecuregcm.entities.ProvisioningMessage; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.push.PushSender; +import org.whispersystems.textsecuregcm.push.WebsocketSender; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.util.Base64; +import org.whispersystems.textsecuregcm.websocket.InvalidWebsocketAddressException; +import org.whispersystems.textsecuregcm.websocket.ProvisioningAddress; + +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; + +import io.dropwizard.auth.Auth; + +@Path("/v1/provisioning") +public class ProvisioningController { + + private final RateLimiters rateLimiters; + private final WebsocketSender websocketSender; + + public ProvisioningController(RateLimiters rateLimiters, PushSender pushSender) { + this.rateLimiters = rateLimiters; + this.websocketSender = pushSender.getWebSocketSender(); + } + + @Timed + @Path("/{destination}") + @PUT + @Consumes(MediaType.APPLICATION_JSON) + public void sendProvisioningMessage(@Auth Account source, + @PathParam("destination") String destinationName, + @Valid ProvisioningMessage message) + throws RateLimitExceededException, InvalidWebsocketAddressException, IOException + { + rateLimiters.getMessagesLimiter().validate(source.getNumber()); + + if (!websocketSender.sendProvisioningMessage(new ProvisioningAddress(destinationName), + Base64.decode(message.getBody()))) + { + throw new WebApplicationException(Response.Status.NOT_FOUND); + } + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/ProvisioningMessage.java b/src/main/java/org/whispersystems/textsecuregcm/entities/ProvisioningMessage.java index c6a5def96..c6fa8518d 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/ProvisioningMessage.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/ProvisioningMessage.java @@ -1,18 +1,14 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.validator.constraints.NotEmpty; public class ProvisioningMessage { @JsonProperty + @NotEmpty private String body; - public ProvisioningMessage() {} - - public ProvisioningMessage(String body) { - this.body = body; - } - public String getBody() { return body; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/SendMessageResponse.java b/src/main/java/org/whispersystems/textsecuregcm/entities/SendMessageResponse.java new file mode 100644 index 000000000..5b4463b57 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/SendMessageResponse.java @@ -0,0 +1,16 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SendMessageResponse { + + @JsonProperty + private boolean needsSync; + + public SendMessageResponse() {} + + public SendMessageResponse(boolean needsSync) { + this.needsSync = needsSync; + } + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index 45deb433e..78c240de1 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -121,6 +121,16 @@ public class Account { return highestDevice + 1; } + public int getActiveDeviceCount() { + int count = 0; + + for (Device device : devices) { + if (device.isActive()) count++; + } + + return count; + } + public boolean isRateLimited() { return true; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/websocket/ProvisioningAddress.java b/src/main/java/org/whispersystems/textsecuregcm/websocket/ProvisioningAddress.java index 174ffecc1..f0019928f 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/websocket/ProvisioningAddress.java +++ b/src/main/java/org/whispersystems/textsecuregcm/websocket/ProvisioningAddress.java @@ -7,17 +7,11 @@ import java.security.SecureRandom; public class ProvisioningAddress extends WebsocketAddress { - private static final String PREFIX = ">>ephemeral-"; - private final String address; public ProvisioningAddress(String address) throws InvalidWebsocketAddressException { super(address, 0); this.address = address; - - if (address == null || !address.startsWith(PREFIX)) { - throw new InvalidWebsocketAddressException(address); - } } public String getAddress() { @@ -29,8 +23,8 @@ public class ProvisioningAddress extends WebsocketAddress { byte[] random = new byte[16]; SecureRandom.getInstance("SHA1PRNG").nextBytes(random); - return new ProvisioningAddress(PREFIX + Base64.encodeBytesWithoutPadding(random) - .replace('+', '-').replace('/', '_')); + return new ProvisioningAddress(Base64.encodeBytesWithoutPadding(random) + .replace('+', '-').replace('/', '_')); } catch (NoSuchAlgorithmException | InvalidWebsocketAddressException e) { throw new AssertionError(e); } diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java index 93a263390..03d9d2f24 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java @@ -82,7 +82,7 @@ public class DeviceControllerTest { @Test public void validDeviceRegisterTest() throws Exception { - VerificationCode deviceCode = resources.client().resource("/v1/devices/provisioning_code") + VerificationCode deviceCode = resources.client().resource("/v1/devices/provisioning/code") .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .get(VerificationCode.class); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java index bb41243b4..290357d51 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java @@ -98,7 +98,7 @@ public class MessageControllerTest { .type(MediaType.APPLICATION_JSON_TYPE) .put(ClientResponse.class); - assertThat("Good Response", response.getStatus(), is(equalTo(204))); + assertThat("Good Response", response.getStatus(), is(equalTo(200))); verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class)); } @@ -148,7 +148,7 @@ public class MessageControllerTest { .type(MediaType.APPLICATION_JSON_TYPE) .put(ClientResponse.class); - assertThat("Good Response Code", response.getStatus(), is(equalTo(204))); + assertThat("Good Response Code", response.getStatus(), is(equalTo(200))); verify(pushSender, times(2)).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.OutgoingMessageSignal.class)); }