diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java index fc47c6038..8029374f6 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerConfiguration.java @@ -107,9 +107,6 @@ public class WhisperServerConfiguration extends Configuration { @JsonProperty private RateLimitsConfiguration limits = new RateLimitsConfiguration(); - @JsonProperty - private RedPhoneConfiguration redphone = new RedPhoneConfiguration(); - @Valid @NotNull @JsonProperty @@ -183,10 +180,6 @@ public class WhisperServerConfiguration extends Configuration { return federation; } - public RedPhoneConfiguration getRedphoneConfiguration() { - return redphone; - } - public TurnConfiguration getTurnConfiguration() { return turn; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 6debf8685..d89ed7112 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -58,7 +58,6 @@ import org.whispersystems.textsecuregcm.metrics.NetworkReceivedGauge; import org.whispersystems.textsecuregcm.metrics.NetworkSentGauge; import org.whispersystems.textsecuregcm.providers.RedisClientFactory; import org.whispersystems.textsecuregcm.providers.RedisHealthCheck; -import org.whispersystems.textsecuregcm.providers.TimeProvider; import org.whispersystems.textsecuregcm.push.APNSender; import org.whispersystems.textsecuregcm.push.ApnFallbackManager; import org.whispersystems.textsecuregcm.push.GCMSender; @@ -186,7 +185,6 @@ public class WhisperServerService extends Application authorizationKey = config.getRedphoneConfiguration().getAuthorizationKey(); apnSender.setApnFallbackManager(apnFallbackManager); environment.lifecycle().manage(apnFallbackManager); @@ -208,7 +206,7 @@ public class WhisperServerService extends Application tokenGenerator; private final TurnTokenGenerator turnTokenGenerator; private final Map testDevices; @@ -92,8 +90,6 @@ public class AccountController { RateLimiters rateLimiters, SmsSender smsSenderFactory, MessagesManager messagesManager, - TimeProvider timeProvider, - Optional authorizationKey, TurnTokenGenerator turnTokenGenerator, Map testDevices) { @@ -102,15 +98,8 @@ public class AccountController { 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())); - } else { - tokenGenerator = Optional.absent(); - } } @Timed @@ -158,6 +147,7 @@ public class AccountController { @Timed @PUT @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) @Path("/code/{verification_code}") public void verifyAccount(@PathParam("verification_code") String verificationCode, @HeaderParam("Authorization") String authorizationHeader, @@ -178,8 +168,26 @@ public class AccountController { throw new WebApplicationException(Response.status(403).build()); } - if (accounts.isRelayListed(number)) { - throw new WebApplicationException(Response.status(417).build()); + Optional existingAccount = accounts.get(number); + + if (existingAccount.isPresent() && + existingAccount.get().getPin().isPresent() && + System.currentTimeMillis() - existingAccount.get().getLastSeen() < TimeUnit.DAYS.toMillis(7)) + { + rateLimiters.getVerifyLimiter().clear(number); + rateLimiters.getPinLimiter().validate(number); + + if (accountAttributes.getPin() == null || + !MessageDigest.isEqual(existingAccount.get().getPin().get().getBytes(), accountAttributes.getPin().getBytes())) + { + long timeRemaining = TimeUnit.DAYS.toMillis(7) - (System.currentTimeMillis() - existingAccount.get().getLastSeen()); + + throw new WebApplicationException(Response.status(423) + .entity(new RegistrationLockFailure(timeRemaining)) + .build()); + } + + rateLimiters.getPinLimiter().clear(number); } createAccount(number, password, userAgent, accountAttributes); @@ -189,54 +197,6 @@ public class AccountController { } } - @Timed - @PUT - @Consumes(MediaType.APPLICATION_JSON) - @Path("/token/{verification_token}") - public void verifyToken(@PathParam("verification_token") String verificationToken, - @HeaderParam("Authorization") String authorizationHeader, - @HeaderParam("X-Signal-Agent") String userAgent, - @Valid AccountAttributes accountAttributes) - throws RateLimitExceededException - { - try { - AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader); - String number = header.getNumber(); - String password = header.getPassword(); - - rateLimiters.getVerifyLimiter().validate(number); - - if (!tokenGenerator.isPresent()) { - logger.debug("Attempt to authorize with key but not configured..."); - throw new WebApplicationException(Response.status(403).build()); - } - - if (!tokenGenerator.get().isValid(verificationToken, number, timeProvider.getCurrentTimeMillis())) { - throw new WebApplicationException(Response.status(403).build()); - } - - createAccount(number, password, userAgent, accountAttributes); - } catch (InvalidAuthorizationHeaderException e) { - logger.info("Bad authorization header", e); - throw new WebApplicationException(Response.status(401).build()); - } - } - - @Timed - @GET - @Path("/token/") - @Produces(MediaType.APPLICATION_JSON) - public AuthorizationToken verifyToken(@Auth Account account) - throws RateLimitExceededException - { - if (!tokenGenerator.isPresent()) { - logger.debug("Attempt to authorize with key but not configured..."); - throw new WebApplicationException(Response.status(404).build()); - } - - return tokenGenerator.get().generateFor(account.getNumber()); - } - @Timed @GET @Path("/turn/") @@ -302,6 +262,23 @@ public class AccountController { accounts.update(account); } + @Timed + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Path("/pin/") + public void setPin(@Auth Account account, @Valid RegistrationLock accountLock) { + account.setPin(accountLock.getPin()); + accounts.update(account); + } + + @Timed + @DELETE + @Path("/pin/") + public void removePin(@Auth Account account) { + account.setPin(null); + accounts.update(account); + } + @Timed @PUT @Path("/attributes/") @@ -321,6 +298,8 @@ public class AccountController { device.setSignalingKey(attributes.getSignalingKey()); device.setUserAgent(userAgent); + account.setPin(attributes.getPin()); + accounts.update(account); } @@ -350,6 +329,7 @@ public class AccountController { Account account = new Account(); account.setNumber(number); account.addDevice(device); + account.setPin(accountAttributes.getPin()); if (accounts.create(account)) { newUserMeter.mark(); diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java b/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java index dd7dd5ec7..5b0081e31 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java @@ -43,21 +43,25 @@ public class AccountAttributes { @JsonProperty private boolean video; + @JsonProperty + private String pin; + public AccountAttributes() {} @VisibleForTesting - public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId) { - this(signalingKey, fetchesMessages, registrationId, null, false, false); + public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String pin) { + this(signalingKey, fetchesMessages, registrationId, null, false, false, pin); } @VisibleForTesting - public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, boolean voice, boolean video) { + public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name, boolean voice, boolean video, String pin) { this.signalingKey = signalingKey; this.fetchesMessages = fetchesMessages; this.registrationId = registrationId; this.name = name; this.voice = voice; this.video = video; + this.pin = pin; } public String getSignalingKey() { @@ -84,4 +88,7 @@ public class AccountAttributes { return video; } + public String getPin() { + return pin; + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLock.java b/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLock.java new file mode 100644 index 000000000..6410a19a2 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLock.java @@ -0,0 +1,26 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.NotEmpty; + +public class RegistrationLock { + + @JsonProperty + @NotEmpty + @Length(min=4,max=20) + private String pin; + + public RegistrationLock() {} + + @VisibleForTesting + public RegistrationLock(String pin) { + this.pin = pin; + } + + public String getPin() { + return pin; + } + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLockFailure.java b/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLockFailure.java new file mode 100644 index 000000000..b57ed6e2a --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/RegistrationLockFailure.java @@ -0,0 +1,21 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class RegistrationLockFailure { + + @JsonProperty + private long timeRemaining; + + public RegistrationLockFailure() {} + + public RegistrationLockFailure(long timeRemaining) { + this.timeRemaining = timeRemaining; + } + + @JsonIgnore + public long getTimeRemaining() { + return timeRemaining; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/limits/LockingRateLimiter.java b/src/main/java/org/whispersystems/textsecuregcm/limits/LockingRateLimiter.java new file mode 100644 index 000000000..26b6ad055 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/limits/LockingRateLimiter.java @@ -0,0 +1,60 @@ +package org.whispersystems.textsecuregcm.limits; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SharedMetricRegistries; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; +import org.whispersystems.textsecuregcm.util.Constants; + +import static com.codahale.metrics.MetricRegistry.name; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +public class LockingRateLimiter extends RateLimiter { + + private final Meter meter; + + public LockingRateLimiter(JedisPool cacheClient, String name, int bucketSize, double leakRatePerMinute) { + super(cacheClient, name, bucketSize, leakRatePerMinute); + + MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); + this.meter = metricRegistry.meter(name(getClass(), name, "locked")); + } + + @Override + public void validate(String key, int amount) throws RateLimitExceededException { + if (!acquireLock(key)) { + meter.mark(); + throw new RateLimitExceededException("Locked"); + } + + try { + super.validate(key, amount); + } finally { + releaseLock(key); + } + } + + @Override + public void validate(String key) throws RateLimitExceededException { + validate(key, 1); + } + + private void releaseLock(String key) { + try (Jedis jedis = cacheClient.getResource()) { + jedis.del(getLockName(key)); + } + } + + private boolean acquireLock(String key) { + try (Jedis jedis = cacheClient.getResource()) { + return jedis.set(getLockName(key), "L", "NX", "EX", 10) != null; + } + } + + private String getLockName(String key) { + return "leaky_lock::" + name + "::" + key; + } + + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiter.java b/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiter.java index ea3d3911c..5da82f6aa 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiter.java +++ b/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiter.java @@ -38,14 +38,22 @@ public class RateLimiter { private final Logger logger = LoggerFactory.getLogger(RateLimiter.class); private final ObjectMapper mapper = SystemMapper.getMapper(); - private final Meter meter; - private final JedisPool cacheClient; - private final String name; - private final int bucketSize; - private final double leakRatePerMillis; + private final Meter meter; + protected final JedisPool cacheClient; + protected final String name; + private final int bucketSize; + private final double leakRatePerMillis; + private final boolean reportLimits; public RateLimiter(JedisPool cacheClient, String name, int bucketSize, double leakRatePerMinute) + { + this(cacheClient, name, bucketSize, leakRatePerMinute, false); + } + + public RateLimiter(JedisPool cacheClient, String name, + int bucketSize, double leakRatePerMinute, + boolean reportLimits) { MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME); @@ -54,6 +62,7 @@ public class RateLimiter { this.name = name; this.bucketSize = bucketSize; this.leakRatePerMillis = leakRatePerMinute / (60.0 * 1000.0); + this.reportLimits = reportLimits; } public void validate(String key, int amount) throws RateLimitExceededException { @@ -71,6 +80,12 @@ public class RateLimiter { validate(key, 1); } + public void clear(String key) { + try (Jedis jedis = cacheClient.getResource()) { + jedis.del(getBucketName(key)); + } + } + private void setBucket(String key, LeakyBucket bucket) { try (Jedis jedis = cacheClient.getResource()) { String serialized = bucket.serialize(mapper); diff --git a/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java b/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java index bc21972f0..1dff0e157 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java +++ b/src/main/java/org/whispersystems/textsecuregcm/limits/RateLimiters.java @@ -27,6 +27,7 @@ public class RateLimiters { private final RateLimiter voiceDestinationLimiter; private final RateLimiter voiceDestinationDailyLimiter; private final RateLimiter verifyLimiter; + private final RateLimiter pinLimiter; private final RateLimiter attachmentLimiter; private final RateLimiter contactsLimiter; @@ -57,6 +58,10 @@ public class RateLimiters { config.getVerifyNumber().getBucketSize(), config.getVerifyNumber().getLeakRatePerMinute()); + this.pinLimiter = new LockingRateLimiter(cacheClient, "pin", + config.getVerifyPin().getBucketSize(), + config.getVerifyPin().getLeakRatePerMinute()); + this.attachmentLimiter = new RateLimiter(cacheClient, "attachmentCreate", config.getAttachments().getBucketSize(), config.getAttachments().getLeakRatePerMinute()); @@ -130,6 +135,10 @@ public class RateLimiters { return verifyLimiter; } + public RateLimiter getPinLimiter() { + return pinLimiter; + } + public RateLimiter getTurnLimiter() { return turnLimiter; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index 48b4535b8..66b8279ee 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify @@ -48,6 +48,9 @@ public class Account { @JsonProperty private String avatarDigest; + @JsonProperty + private String pin; + @JsonIgnore private Device authenticatedDevice; @@ -204,4 +207,12 @@ public class Account { public void setAvatarDigest(String avatarDigest) { this.avatarDigest = avatarDigest; } + + public Optional getPin() { + return Optional.fromNullable(pin); + } + + public void setPin(String pin) { + this.pin = pin; + } } 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 f8b927ee1..0e40c8bd9 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java @@ -1,8 +1,6 @@ package org.whispersystems.textsecuregcm.tests.controllers; import com.google.common.base.Optional; -import org.apache.commons.codec.DecoderException; -import org.apache.commons.codec.binary.Hex; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.junit.Before; import org.junit.Rule; @@ -11,9 +9,13 @@ import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider; import org.whispersystems.textsecuregcm.auth.StoredVerificationCode; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.controllers.AccountController; +import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.entities.RegistrationLock; +import org.whispersystems.textsecuregcm.entities.RegistrationLockFailure; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper; import org.whispersystems.textsecuregcm.providers.TimeProvider; import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.storage.Account; @@ -36,23 +38,27 @@ import static org.mockito.Mockito.*; public class AccountControllerTest { - private static final String SENDER = "+14152222222"; - private static final String SENDER_OLD = "+14151111111"; + private static final String SENDER = "+14152222222"; + private static final String SENDER_OLD = "+14151111111"; + private static final String SENDER_PIN = "+14153333333"; + private static final String SENDER_OVER_PIN = "+14154444444"; private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class); private AccountsManager accountsManager = mock(AccountsManager.class ); private RateLimiters rateLimiters = mock(RateLimiters.class ); private RateLimiter rateLimiter = mock(RateLimiter.class ); + private RateLimiter pinLimiter = mock(RateLimiter.class ); 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"); + private Account senderPinAccount = mock(Account.class); @Rule public final ResourceTestRule resources = ResourceTestRule.builder() .addProvider(AuthHelper.getAuthFilter()) .addProvider(new AuthValueFactoryProvider.Binder()) + .addProvider(new RateLimitExceededExceptionMapper()) .setMapper(SystemMapper.getMapper()) .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) .addResource(new AccountController(pendingAccountsManager, @@ -60,10 +66,8 @@ public class AccountControllerTest { rateLimiters, smsSender, storedMessages, - timeProvider, - Optional.of(authorizationKey), turnTokenGenerator, - new HashMap())) + new HashMap<>())) .build(); @@ -72,11 +76,25 @@ public class AccountControllerTest { when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter); when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter); when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getPinLimiter()).thenReturn(pinLimiter); when(timeProvider.getCurrentTimeMillis()).thenReturn(System.currentTimeMillis()); + when(senderPinAccount.getPin()).thenReturn(Optional.of("31337")); + when(senderPinAccount.getLastSeen()).thenReturn(System.currentTimeMillis()); + + when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis()))); when(pendingAccountsManager.getCodeForNumber(SENDER_OLD)).thenReturn(Optional.of(new StoredVerificationCode("1234", System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(31)))); + when(pendingAccountsManager.getCodeForNumber(SENDER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("333333", System.currentTimeMillis()))); + when(pendingAccountsManager.getCodeForNumber(SENDER_OVER_PIN)).thenReturn(Optional.of(new StoredVerificationCode("444444", System.currentTimeMillis()))); + + when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount)); + when(accountsManager.get(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount)); + when(accountsManager.get(eq(SENDER))).thenReturn(Optional.absent()); + when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.absent()); + + doThrow(new RateLimitExceededException(SENDER_OVER_PIN)).when(pinLimiter).validate(eq(SENDER_OVER_PIN)); } @Test @@ -89,7 +107,7 @@ public class AccountControllerTest { assertThat(response.getStatus()).isEqualTo(200); - verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.absent()), anyString()); + verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.absent()), anyString()); } @Test @@ -113,7 +131,7 @@ public class AccountControllerTest { .target(String.format("/v1/accounts/code/%s", "1234")) .request() .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222, null), MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(204); @@ -128,7 +146,7 @@ public class AccountControllerTest { .target(String.format("/v1/accounts/code/%s", "1234")) .request() .header("Authorization", AuthHelper.getAuthHeader(SENDER_OLD, "bar")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 2222, null), MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(403); @@ -143,7 +161,7 @@ public class AccountControllerTest { .target(String.format("/v1/accounts/code/%s", "1111")) .request() .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null), MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(403); @@ -152,87 +170,116 @@ public class AccountControllerTest { } @Test - public void testVerifyToken() throws Exception { - when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L); - - String token = SENDER + ":1415906573:af4f046107c21721224a"; - + public void testVerifyPin() throws Exception { Response response = resources.getJerseyTest() - .target(String.format("/v1/accounts/token/%s", token)) + .target(String.format("/v1/accounts/code/%s", "333333")) .request() - .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444), + .header("Authorization", AuthHelper.getAuthHeader(SENDER_PIN, "bar")) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, "31337"), MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(204); - verify(accountsManager, times(1)).create(isA(Account.class)); + verify(pinLimiter).validate(eq(SENDER_PIN)); } @Test - public void testVerifyBadToken() throws Exception { - when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L); - - String token = SENDER + ":1415906574:af4f046107c21721224a"; - + public void testVerifyWrongPin() throws Exception { Response response = resources.getJerseyTest() - .target(String.format("/v1/accounts/token/%s", token)) + .target(String.format("/v1/accounts/code/%s", "333333")) .request() - .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444), + .header("Authorization", AuthHelper.getAuthHeader(SENDER_PIN, "bar")) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, "31338"), MediaType.APPLICATION_JSON_TYPE)); - assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getStatus()).isEqualTo(423); - verifyNoMoreInteractions(accountsManager); + verify(pinLimiter).validate(eq(SENDER_PIN)); } @Test - public void testVerifyWrongToken() throws Exception { - when(timeProvider.getCurrentTimeMillis()).thenReturn(1415917053106L); - - String token = SENDER + ":1415906573:af4f046107c21721224a"; - + public void testVerifyNoPin() throws Exception { Response response = resources.getJerseyTest() - .target(String.format("/v1/accounts/token/%s", token)) + .target(String.format("/v1/accounts/code/%s", "333333")) .request() - .header("Authorization", AuthHelper.getAuthHeader("+14151111111", "bar")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444), + .header("Authorization", AuthHelper.getAuthHeader(SENDER_PIN, "bar")) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null), MediaType.APPLICATION_JSON_TYPE)); - assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getStatus()).isEqualTo(423); - verifyNoMoreInteractions(accountsManager); + RegistrationLockFailure failure = response.readEntity(RegistrationLockFailure.class); + + verify(pinLimiter).validate(eq(SENDER_PIN)); } @Test - public void testVerifyExpiredToken() throws Exception { - when(timeProvider.getCurrentTimeMillis()).thenReturn(1416003757901L); - - String token = SENDER + ":1415906573:af4f046107c21721224a"; - + public void testVerifyLimitPin() throws Exception { Response response = resources.getJerseyTest() - .target(String.format("/v1/accounts/token/%s", token)) + .target(String.format("/v1/accounts/code/%s", "444444")) .request() - .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 4444), + .header("Authorization", AuthHelper.getAuthHeader(SENDER_OVER_PIN, "bar")) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, "31337"), MediaType.APPLICATION_JSON_TYPE)); - assertThat(response.getStatus()).isEqualTo(403); + assertThat(response.getStatus()).isEqualTo(413); - verifyNoMoreInteractions(accountsManager); + verify(rateLimiter).clear(eq(SENDER_OVER_PIN)); } - private static byte[] decodeHex(String hex) { + @Test + public void testVerifyOldPin() throws Exception { try { - return Hex.decodeHex(hex.toCharArray()); - } catch (DecoderException e) { - throw new AssertionError(e); + when(senderPinAccount.getLastSeen()).thenReturn(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)); + + Response response = + resources.getJerseyTest() + .target(String.format("/v1/accounts/code/%s", "444444")) + .request() + .header("Authorization", AuthHelper.getAuthHeader(SENDER_OVER_PIN, "bar")) + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 3333, null), + MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatus()).isEqualTo(204); + + } finally { + when(senderPinAccount.getLastSeen()).thenReturn(System.currentTimeMillis()); } } + + @Test + public void testSetPin() throws Exception { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/pin/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new RegistrationLock("31337"))); + + assertThat(response.getStatus()).isEqualTo(204); + + verify(AuthHelper.VALID_ACCOUNT).setPin(eq("31337")); + } + + @Test + public void testSetShortPin() throws Exception { + Response response = + resources.getJerseyTest() + .target("/v1/accounts/pin/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .put(Entity.json(new RegistrationLock("313"))); + + assertThat(response.getStatus()).isEqualTo(422); + + verify(AuthHelper.VALID_ACCOUNT, never()).setPin(anyString()); + } + + + } \ No newline at end of file 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 321fe5c4a..73a7531cb 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java @@ -125,7 +125,7 @@ public class DeviceControllerTest { .target("/v1/devices/5678901") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, null), MediaType.APPLICATION_JSON_TYPE), DeviceResponse.class); @@ -149,7 +149,7 @@ public class DeviceControllerTest { .target("/v1/devices/5678902") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, null), MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(403); @@ -163,7 +163,7 @@ public class DeviceControllerTest { .target("/v1/devices/1112223") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO)) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, null), MediaType.APPLICATION_JSON_TYPE)); assertThat(response.getStatus()).isEqualTo(403); @@ -189,7 +189,7 @@ public class DeviceControllerTest { .target("/v1/devices/5678901") .request() .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters", true, true), + .put(Entity.entity(new AccountAttributes("keykeykeykey", false, 1234, "this is a really long name that is longer than 80 characters", true, true, null), MediaType.APPLICATION_JSON_TYPE)); assertEquals(response.getStatus(), 422);