From 19a4c7253a9f89273f057d7a993859f079957816 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Tue, 1 Nov 2016 16:27:34 -0700 Subject: [PATCH] Support for turn allocations // FREEBIE --- .../WhisperServerConfiguration.java | 10 +++++ .../textsecuregcm/WhisperServerService.java | 7 ++-- .../textsecuregcm/auth/TurnToken.java | 23 +++++++++++ .../auth/TurnTokenGenerator.java | 39 +++++++++++++++++++ .../RateLimitsConfiguration.java | 7 ++++ .../configuration/TurnConfiguration.java | 26 +++++++++++++ .../controllers/AccountController.java | 30 +++++++++----- .../textsecuregcm/limits/RateLimiters.java | 9 +++++ .../controllers/AccountControllerTest.java | 3 ++ 9 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/auth/TurnToken.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index d16e92398..bfa78ef3c 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -25,6 +25,7 @@ import org.whispersystems.textsecuregcm.configuration.RedPhoneConfiguration; import org.whispersystems.textsecuregcm.configuration.RedisConfiguration; import org.whispersystems.textsecuregcm.configuration.S3Configuration; import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration; +import org.whispersystems.textsecuregcm.configuration.TurnConfiguration; import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration; import org.whispersystems.textsecuregcm.configuration.WebsocketConfiguration; @@ -105,6 +106,11 @@ public class WhisperServerConfiguration extends Configuration { @JsonProperty private JerseyClientConfiguration httpClient = new JerseyClientConfiguration(); + @Valid + @NotNull + @JsonProperty + private TurnConfiguration turn; + public WebsocketConfiguration getWebsocketConfiguration() { return websocket; @@ -158,6 +164,10 @@ public class WhisperServerConfiguration extends Configuration { return redphone; } + public TurnConfiguration getTurnConfiguration() { + return turn; + } + public Map getTestDevices() { Map results = new HashMap<>(); diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 66b8fdeb0..b24ab0ddc 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -17,7 +17,6 @@ package org.whispersystems.textsecuregcm; import com.codahale.metrics.SharedMetricRegistries; -import com.codahale.metrics.graphite.GraphiteReporter; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -33,6 +32,7 @@ import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider; import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter; import org.whispersystems.textsecuregcm.auth.AccountAuthenticator; import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator; +import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.controllers.AccountController; import org.whispersystems.textsecuregcm.controllers.AttachmentController; import org.whispersystems.textsecuregcm.controllers.DeviceController; @@ -100,14 +100,12 @@ import javax.servlet.ServletRegistration; import javax.ws.rs.client.Client; import java.security.Security; import java.util.EnumSet; -import java.util.concurrent.TimeUnit; import static com.codahale.metrics.MetricRegistry.name; import io.dropwizard.Application; import io.dropwizard.client.JerseyClientBuilder; import io.dropwizard.db.DataSourceFactory; import io.dropwizard.jdbi.DBIFactory; -import io.dropwizard.metrics.graphite.GraphiteReporterFactory; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; import redis.clients.jedis.JedisPool; @@ -190,6 +188,7 @@ public class WhisperServerService extends Application authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey(); environment.lifecycle().manage(apnFallbackManager); @@ -212,7 +211,7 @@ public class WhisperServerService extends Application urls; + + public TurnToken(String username, String password, List urls) { + this.username = username; + this.password = password; + this.urls = urls; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java b/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java new file mode 100644 index 000000000..216f84a91 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/auth/TurnTokenGenerator.java @@ -0,0 +1,39 @@ +package org.whispersystems.textsecuregcm.auth; + +import org.whispersystems.textsecuregcm.configuration.TurnConfiguration; +import org.whispersystems.textsecuregcm.util.Base64; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class TurnTokenGenerator { + + private final byte[] key; + private final List urls; + + public TurnTokenGenerator(TurnConfiguration configuration) { + this.key = configuration.getSecret().getBytes(); + this.urls = configuration.getUris(); + } + + public TurnToken generate() { + try { + Mac mac = Mac.getInstance("HmacSHA1"); + long validUntilSeconds = (System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1)) / 1000; + long user = Math.abs(SecureRandom.getInstance("SHA1PRNG").nextInt()); + String userTime = validUntilSeconds + ":" + user; + + mac.init(new SecretKeySpec(key, "HmacSHA1")); + String password = Base64.encodeBytes(mac.doFinal(userTime.getBytes())); + + return new TurnToken(userTime, password, urls); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new AssertionError(e); + } + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/configuration/RateLimitsConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/configuration/RateLimitsConfiguration.java index b11d0eb5d..e313cc758 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/configuration/RateLimitsConfiguration.java +++ b/src/main/java/org/whispersystems/textsecuregcm/configuration/RateLimitsConfiguration.java @@ -50,6 +50,9 @@ public class RateLimitsConfiguration { @JsonProperty private RateLimitConfiguration verifyDevice = new RateLimitConfiguration(2, 2); + @JsonProperty + private RateLimitConfiguration turnAllocations = new RateLimitConfiguration(60, 60); + public RateLimitConfiguration getAllocateDevice() { return allocateDevice; } @@ -90,6 +93,10 @@ public class RateLimitsConfiguration { return verifyNumber; } + public RateLimitConfiguration getTurnAllocations() { + return turnAllocations; + } + public static class RateLimitConfiguration { @JsonProperty private int bucketSize; diff --git a/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java new file mode 100644 index 000000000..dcb13ff39 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/configuration/TurnConfiguration.java @@ -0,0 +1,26 @@ +package org.whispersystems.textsecuregcm.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.constraints.NotNull; +import java.util.List; + +public class TurnConfiguration { + + @JsonProperty + @NotEmpty + private String secret; + + @JsonProperty + @NotNull + private List uris; + + public List getUris() { + return uris; + } + + public String getSecret() { + return secret; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index 365d6962d..7f5a35838 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -19,7 +19,6 @@ package org.whispersystems.textsecuregcm.controllers; import com.codahale.metrics.Meter; import com.codahale.metrics.MetricRegistry; import com.codahale.metrics.SharedMetricRegistries; -import com.codahale.metrics.Timer; import com.codahale.metrics.annotation.Timed; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; @@ -30,6 +29,8 @@ import org.whispersystems.textsecuregcm.auth.AuthorizationHeader; import org.whispersystems.textsecuregcm.auth.AuthorizationToken; import org.whispersystems.textsecuregcm.auth.AuthorizationTokenGenerator; import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException; +import org.whispersystems.textsecuregcm.auth.TurnToken; +import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.entities.ApnRegistrationId; import org.whispersystems.textsecuregcm.entities.GcmRegistrationId; @@ -45,7 +46,6 @@ import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.VerificationCode; -import org.whispersystems.textsecuregcm.websocket.WebSocketConnection; import javax.validation.Valid; import javax.ws.rs.Consumes; @@ -83,6 +83,7 @@ public class AccountController { private final MessagesManager messagesManager; private final TimeProvider timeProvider; private final Optional tokenGenerator; + private final TurnTokenGenerator turnTokenGenerator; private final Map testDevices; public AccountController(PendingAccountsManager pendingAccounts, @@ -92,15 +93,17 @@ public class AccountController { MessagesManager messagesManager, TimeProvider timeProvider, Optional authorizationKey, + TurnTokenGenerator turnTokenGenerator, Map testDevices) { - this.pendingAccounts = pendingAccounts; - this.accounts = accounts; - this.rateLimiters = rateLimiters; - this.smsSender = smsSenderFactory; - this.messagesManager = messagesManager; - this.timeProvider = timeProvider; - this.testDevices = testDevices; + this.pendingAccounts = pendingAccounts; + this.accounts = accounts; + this.rateLimiters = rateLimiters; + this.smsSender = smsSenderFactory; + this.messagesManager = messagesManager; + this.timeProvider = timeProvider; + this.testDevices = testDevices; + this.turnTokenGenerator = turnTokenGenerator; if (authorizationKey.isPresent()) { tokenGenerator = Optional.of(new AuthorizationTokenGenerator(authorizationKey.get())); @@ -232,6 +235,15 @@ public class AccountController { return tokenGenerator.get().generateFor(account.getNumber()); } + @Timed + @GET + @Path("/turn/") + @Produces(MediaType.APPLICATION_JSON) + public TurnToken getTurnToken(@Auth Account account) throws RateLimitExceededException { + rateLimiters.getTurnLimiter().validate(account.getNumber()); + return turnTokenGenerator.generate(); + } + @Timed @PUT @Path("/gcm/") diff --git a/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java b/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java index 9bbc4182f..00ad31c10 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java +++ b/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java @@ -36,6 +36,8 @@ public class RateLimiters { private final RateLimiter allocateDeviceLimiter; private final RateLimiter verifyDeviceLimiter; + private final RateLimiter turnLimiter; + public RateLimiters(RateLimitsConfiguration config, JedisPool cacheClient) { this.smsDestinationLimiter = new RateLimiter(cacheClient, "smsDestination", config.getSmsDestination().getBucketSize(), @@ -77,6 +79,9 @@ public class RateLimiters { config.getVerifyDevice().getBucketSize(), config.getVerifyDevice().getLeakRatePerMinute()); + this.turnLimiter = new RateLimiter(cacheClient, "turnAllocate", + config.getTurnAllocations().getBucketSize(), + config.getTurnAllocations().getLeakRatePerMinute()); } public RateLimiter getAllocateDeviceLimiter() { @@ -119,4 +124,8 @@ public class RateLimiters { return verifyLimiter; } + public RateLimiter getTurnLimiter() { + return turnLimiter; + } + } diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java index 11aa87cc1..8855a3e86 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java @@ -8,6 +8,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider; +import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.controllers.AccountController; import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.limits.RateLimiter; @@ -42,6 +43,7 @@ public class AccountControllerTest { private SmsSender smsSender = mock(SmsSender.class ); private MessagesManager storedMessages = mock(MessagesManager.class ); private TimeProvider timeProvider = mock(TimeProvider.class ); + private TurnTokenGenerator turnTokenGenerator = mock(TurnTokenGenerator.class); private static byte[] authorizationKey = decodeHex("3a078586eea8971155f5c1ebd73c8c923cbec1c3ed22a54722e4e88321dc749f"); @Rule @@ -57,6 +59,7 @@ public class AccountControllerTest { storedMessages, timeProvider, Optional.of(authorizationKey), + turnTokenGenerator, new HashMap())) .build();