diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 4170cc9f..eb39f035 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -32,6 +32,7 @@ import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator; import org.whispersystems.textsecuregcm.auth.MultiBasicAuthProvider; import org.whispersystems.textsecuregcm.configuration.NexmoConfiguration; import org.whispersystems.textsecuregcm.controllers.AccountController; +import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.controllers.AttachmentController; import org.whispersystems.textsecuregcm.controllers.DirectoryController; import org.whispersystems.textsecuregcm.controllers.FederationController; @@ -125,7 +126,8 @@ public class WhisperServerService extends Service { accountAuthenticator, Account.class, "WhisperServer")); - environment.addResource(new AccountController(pendingAccountsManager, pendingDevicesManager, accountsManager, rateLimiters, smsSender)); + environment.addResource(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender)); + environment.addResource(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters)); environment.addResource(new DirectoryController(rateLimiters, directory)); environment.addResource(new AttachmentController(rateLimiters, federatedClientManager, urlSigner)); environment.addResource(new KeysController(rateLimiters, keys, accountsManager, federatedClientManager)); diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index 3a636448..5372d7d4 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -61,19 +61,16 @@ public class AccountController { private final Logger logger = LoggerFactory.getLogger(AccountController.class); private final PendingAccountsManager pendingAccounts; - private final PendingDevicesManager pendingDevices; private final AccountsManager accounts; private final RateLimiters rateLimiters; private final SmsSender smsSender; public AccountController(PendingAccountsManager pendingAccounts, - PendingDevicesManager pendingDevices, - AccountsManager accounts, - RateLimiters rateLimiters, - SmsSender smsSenderFactory) + AccountsManager accounts, + RateLimiters rateLimiters, + SmsSender smsSenderFactory) { this.pendingAccounts = pendingAccounts; - this.pendingDevices = pendingDevices; this.accounts = accounts; this.rateLimiters = rateLimiters; this.smsSender = smsSenderFactory; @@ -213,65 +210,4 @@ public class AccountController { throw new AssertionError(e); } } - - @Timed - @GET - @Path("/registerdevice") - @Produces(MediaType.APPLICATION_JSON) - public VerificationCode createDeviceToken(@Auth Account account) - throws RateLimitExceededException - { - rateLimiters.getVerifyLimiter().validate(account.getNumber()); //TODO: New limiter? - - VerificationCode verificationCode = generateVerificationCode(); - pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode()); - - return verificationCode; - } - - @Timed - @PUT - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - @Path("/device/{verification_code}") - public long verifyDeviceToken(@PathParam("verification_code") String verificationCode, - @HeaderParam("Authorization") String authorizationHeader, - @Valid AccountAttributes accountAttributes) - throws RateLimitExceededException - { - Account account; - try { - AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader); - String number = header.getNumber(); - String password = header.getPassword(); - - rateLimiters.getVerifyLimiter().validate(number); //TODO: New limiter? - - Optional storedVerificationCode = pendingDevices.getCodeForNumber(number); - - if (!storedVerificationCode.isPresent() || - !verificationCode.equals(storedVerificationCode.get())) - { - throw new WebApplicationException(Response.status(403).build()); - } - - account = new Account(); - account.setNumber(number); - account.setAuthenticationCredentials(new AuthenticationCredentials(password)); - account.setSignalingKey(accountAttributes.getSignalingKey()); - account.setSupportsSms(accountAttributes.getSupportsSms()); - account.setFetchesMessages(accountAttributes.getFetchesMessages()); - - accounts.createAccountOnExistingNumber(account); - - pendingDevices.remove(number); - - logger.debug("Stored new device account..."); - } catch (InvalidAuthorizationHeaderException e) { - logger.info("Bad Authorization Header", e); - throw new WebApplicationException(Response.status(401).build()); - } - - return account.getDeviceId(); - } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java new file mode 100644 index 00000000..4fdaba60 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java @@ -0,0 +1,137 @@ +/** + * Copyright (C) 2013 Open WhisperSystems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package org.whispersystems.textsecuregcm.controllers; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Optional; +import com.yammer.dropwizard.auth.Auth; +import com.yammer.metrics.annotation.Timed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; +import org.whispersystems.textsecuregcm.auth.AuthorizationHeader; +import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.PendingDevicesManager; +import org.whispersystems.textsecuregcm.util.VerificationCode; + +import javax.validation.Valid; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +@Path("/v1/devices") +public class DeviceController { + + private final Logger logger = LoggerFactory.getLogger(DeviceController.class); + + private final PendingDevicesManager pendingDevices; + private final AccountsManager accounts; + private final RateLimiters rateLimiters; + + public DeviceController(PendingDevicesManager pendingDevices, + AccountsManager accounts, + RateLimiters rateLimiters) + { + this.pendingDevices = pendingDevices; + this.accounts = accounts; + this.rateLimiters = rateLimiters; + } + + @Timed + @GET + @Path("") + @Produces(MediaType.APPLICATION_JSON) + public VerificationCode createDeviceToken(@Auth Account account) + throws RateLimitExceededException + { + rateLimiters.getVerifyLimiter().validate(account.getNumber()); //TODO: New limiter? + + VerificationCode verificationCode = generateVerificationCode(); + pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode()); + + return verificationCode; + } + + @Timed + @PUT + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + @Path("/{verification_code}") + public long verifyDeviceToken(@PathParam("verification_code") String verificationCode, + @HeaderParam("Authorization") String authorizationHeader, + @Valid AccountAttributes accountAttributes) + throws RateLimitExceededException + { + Account account; + try { + AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader); + String number = header.getNumber(); + String password = header.getPassword(); + + rateLimiters.getVerifyLimiter().validate(number); //TODO: New limiter? + + Optional storedVerificationCode = pendingDevices.getCodeForNumber(number); + + if (!storedVerificationCode.isPresent() || + !verificationCode.equals(storedVerificationCode.get())) + { + throw new WebApplicationException(Response.status(403).build()); + } + + account = new Account(); + account.setNumber(number); + account.setAuthenticationCredentials(new AuthenticationCredentials(password)); + account.setSignalingKey(accountAttributes.getSignalingKey()); + account.setSupportsSms(accountAttributes.getSupportsSms()); + account.setFetchesMessages(accountAttributes.getFetchesMessages()); + + accounts.createAccountOnExistingNumber(account); + + pendingDevices.remove(number); + + logger.debug("Stored new device account..."); + } catch (InvalidAuthorizationHeaderException e) { + logger.info("Bad Authorization Header", e); + throw new WebApplicationException(Response.status(401).build()); + } + + return account.getDeviceId(); + } + + @VisibleForTesting protected VerificationCode generateVerificationCode() { + try { + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + int randomInt = 100000 + random.nextInt(900000); + return new VerificationCode(randomInt); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java index ca2dac79..6b54acc8 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java @@ -46,7 +46,7 @@ import java.util.List; @Path("/v1/keys") public class KeysController { - private final Logger logger = LoggerFactory.getLogger(AccountController.class); + private final Logger logger = LoggerFactory.getLogger(DeviceController.class); private final RateLimiters rateLimiters; private final Keys keys; diff --git a/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java b/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java index 41d56cad..b4b559ed 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java +++ b/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java @@ -69,4 +69,8 @@ public class VerificationCode { @VisibleForTesting public boolean equals(Object o) { return o instanceof VerificationCode && verificationCode.equals(((VerificationCode) o).verificationCode); } + + public int hashCode() { + return Integer.parseInt(verificationCode); + } } diff --git a/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java index 38ce3dab..ecc7643b 100644 --- a/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java +++ b/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java @@ -11,6 +11,7 @@ import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.whispersystems.textsecuregcm.controllers.AccountController; +import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; @@ -48,8 +49,8 @@ public class AccountControllerTest extends ResourceTest { @Path("/v1/accounts") static class DumbVerificationAccountController extends AccountController { - public DumbVerificationAccountController(PendingAccountsManager pendingAccounts, PendingDevicesManager pendingDevices, AccountsManager accounts, RateLimiters rateLimiters, SmsSender smsSenderFactory) { - super(pendingAccounts, pendingDevices, accounts, rateLimiters, smsSenderFactory); + public DumbVerificationAccountController(PendingAccountsManager pendingAccounts, AccountsManager accounts, RateLimiters rateLimiters, SmsSender smsSenderFactory) { + super(pendingAccounts, accounts, rateLimiters, smsSenderFactory); } @Override @@ -61,7 +62,6 @@ public class AccountControllerTest extends ResourceTest { private static final String SENDER = "+14152222222"; private PendingAccountsManager pendingAccountsManager = mock(PendingAccountsManager.class); - private PendingDevicesManager pendingDevicesManager = mock(PendingDevicesManager.class); private AccountsManager accountsManager = mock(AccountsManager.class ); private RateLimiters rateLimiters = mock(RateLimiters.class ); private RateLimiter rateLimiter = mock(RateLimiter.class ); @@ -77,8 +77,6 @@ public class AccountControllerTest extends ResourceTest { when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234")); - when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of("5678901")); - Mockito.doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { @@ -87,7 +85,7 @@ public class AccountControllerTest extends ResourceTest { } }).when(accountsManager).createAccountOnExistingNumber(any(Account.class)); - addResource(new DumbVerificationAccountController(pendingAccountsManager, pendingDevicesManager, accountsManager, rateLimiters, smsSender)); + addResource(new DumbVerificationAccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender)); } @Test @@ -133,28 +131,4 @@ public class AccountControllerTest extends ResourceTest { verifyNoMoreInteractions(accountsManager); } - @Test - public void validDeviceRegisterTest() throws Exception { - VerificationCode deviceCode = client().resource("/v1/accounts/registerdevice") - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) - .get(VerificationCode.class); - - assertThat(deviceCode).isEqualTo(new VerificationCode(5678901)); - - Long deviceId = client().resource(String.format("/v1/accounts/device/5678901")) - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) - .entity(new AccountAttributes("keykeykeykey", false, true)) - .type(MediaType.APPLICATION_JSON_TYPE) - .put(Long.class); - assertThat(deviceId).isNotEqualTo(AuthHelper.DEFAULT_DEVICE_ID); - - ArgumentCaptor newAccount = ArgumentCaptor.forClass(Account.class); - verify(accountsManager).createAccountOnExistingNumber(newAccount.capture()); - assertThat(deviceId).isEqualTo(newAccount.getValue().getDeviceId()); - - ArgumentCaptor number = ArgumentCaptor.forClass(String.class); - verify(pendingDevicesManager).remove(number.capture()); - assertThat(number.getValue()).isEqualTo(AuthHelper.VALID_NUMBER); - } - } diff --git a/src/test/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java b/src/test/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java new file mode 100644 index 00000000..cdd739e8 --- /dev/null +++ b/src/test/org/whispersystems/textsecuregcm/tests/controllers/DeviceControllerTest.java @@ -0,0 +1,93 @@ +package org.whispersystems.textsecuregcm.tests.controllers; + +import com.google.common.base.Optional; +import com.yammer.dropwizard.testing.ResourceTest; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.whispersystems.textsecuregcm.controllers.DeviceController; +import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.limits.RateLimiter; +import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.PendingDevicesManager; +import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import org.whispersystems.textsecuregcm.util.VerificationCode; + +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DeviceControllerTest extends ResourceTest { + @Path("/v1/devices") + static class DumbVerificationDeviceController extends DeviceController { + public DumbVerificationDeviceController(PendingDevicesManager pendingDevices, AccountsManager accounts, RateLimiters rateLimiters) { + super(pendingDevices, accounts, rateLimiters); + } + + @Override + protected VerificationCode generateVerificationCode() { + return new VerificationCode(5678901); + } + } + + private static final String SENDER = "+14152222222"; + + private PendingDevicesManager pendingDevicesManager = mock(PendingDevicesManager.class); + private AccountsManager accountsManager = mock(AccountsManager.class ); + private RateLimiters rateLimiters = mock(RateLimiters.class ); + private RateLimiter rateLimiter = mock(RateLimiter.class ); + + @Override + protected void setUpResources() throws Exception { + addProvider(AuthHelper.getAuthenticator()); + + when(rateLimiters.getSmsDestinationLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getVoiceDestinationLimiter()).thenReturn(rateLimiter); + when(rateLimiters.getVerifyLimiter()).thenReturn(rateLimiter); + + when(pendingDevicesManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(Optional.of("5678901")); + + Mockito.doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ((Account) invocation.getArguments()[0]).setDeviceId(2); + return null; + } + }).when(accountsManager).createAccountOnExistingNumber(any(Account.class)); + + addResource(new DumbVerificationDeviceController(pendingDevicesManager, accountsManager, rateLimiters)); + } + + @Test + public void validDeviceRegisterTest() throws Exception { + VerificationCode deviceCode = client().resource("/v1/devices/") + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(VerificationCode.class); + + assertThat(deviceCode).isEqualTo(new VerificationCode(5678901)); + + Long deviceId = client().resource(String.format("/v1/devices/5678901")) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .entity(new AccountAttributes("keykeykeykey", false, true)) + .type(MediaType.APPLICATION_JSON_TYPE) + .put(Long.class); + assertThat(deviceId).isNotEqualTo(AuthHelper.DEFAULT_DEVICE_ID); + + ArgumentCaptor newAccount = ArgumentCaptor.forClass(Account.class); + verify(accountsManager).createAccountOnExistingNumber(newAccount.capture()); + assertThat(deviceId).isEqualTo(newAccount.getValue().getDeviceId()); + + ArgumentCaptor number = ArgumentCaptor.forClass(String.class); + verify(pendingDevicesManager).remove(number.capture()); + assertThat(number.getValue()).isEqualTo(AuthHelper.VALID_NUMBER); + } +}