From ef1160eda850892a6cdf4eea705cd76058cef8c4 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 7 Jan 2014 13:35:58 -1000 Subject: [PATCH] New API to support multiple accounts per # (FREEBIE) --- pom.xml | 2 +- protobuf/OutgoingMessageSignal.proto | 1 + .../textsecuregcm/WhisperServerService.java | 20 +- .../auth/AccountAuthenticator.java | 8 +- .../auth/AuthorizationHeader.java | 34 +- .../controllers/AccountController.java | 97 +- .../controllers/FederationController.java | 25 +- .../controllers/KeysController.java | 72 +- .../controllers/MessageController.java | 22 +- .../entities/AccountAttributes.java | 10 +- .../entities/IncomingMessage.java | 11 + .../textsecuregcm/entities/MessageProtos.java | 866 ++++++++++++++---- .../textsecuregcm/entities/PreKey.java | 15 +- .../textsecuregcm/entities/RelayMessage.java | 10 +- .../entities/UnstructuredPreKeyList.java | 53 ++ .../federation/FederatedClient.java | 9 +- .../textsecuregcm/push/PushSender.java | 27 +- .../textsecuregcm/storage/Account.java | 29 +- .../textsecuregcm/storage/Accounts.java | 111 ++- .../storage/AccountsManager.java | 63 +- .../textsecuregcm/storage/Keys.java | 44 +- .../storage/PendingAccounts.java | 2 + .../storage/PendingAccountsManager.java | 6 + .../storage/PendingDeviceRegistrations.java | 34 + .../storage/PendingDevicesManager.java | 68 ++ .../storage/StoredMessageManager.java | 30 + .../textsecuregcm/storage/StoredMessages.java | 33 + .../textsecuregcm/util/NumberData.java | 25 + .../textsecuregcm/util/VerificationCode.java | 15 + .../workers/DirectoryUpdater.java | 17 +- src/main/resources/migrations.xml | 47 + .../controllers/AccountControllerTest.java | 96 +- .../tests/controllers/KeyControllerTest.java | 69 +- .../tests/entities/PreKeyTest.java | 2 +- .../textsecuregcm/tests/util/AuthHelper.java | 6 +- 35 files changed, 1591 insertions(+), 388 deletions(-) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/entities/UnstructuredPreKeyList.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/storage/PendingDeviceRegistrations.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessageManager.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessages.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/util/NumberData.java diff --git a/pom.xml b/pom.xml index c22e45e02..1ad6ab003 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ com.google.protobuf protobuf-java - 2.4.1 + 2.5.0 diff --git a/protobuf/OutgoingMessageSignal.proto b/protobuf/OutgoingMessageSignal.proto index 06cf14600..f89daf3bd 100644 --- a/protobuf/OutgoingMessageSignal.proto +++ b/protobuf/OutgoingMessageSignal.proto @@ -24,6 +24,7 @@ message OutgoingMessageSignal { optional string source = 2; optional string relay = 3; repeated string destinations = 4; + repeated uint64 destinationDeviceIds = 7; optional uint64 timestamp = 5; optional bytes message = 6; } \ No newline at end of file diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index f6b2992b6..bbe0cdc27 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -57,6 +57,10 @@ import org.whispersystems.textsecuregcm.storage.DirectoryManager; import org.whispersystems.textsecuregcm.storage.Keys; import org.whispersystems.textsecuregcm.storage.PendingAccounts; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; +import org.whispersystems.textsecuregcm.storage.PendingDeviceRegistrations; +import org.whispersystems.textsecuregcm.storage.PendingDevicesManager; +import org.whispersystems.textsecuregcm.storage.StoredMessageManager; +import org.whispersystems.textsecuregcm.storage.StoredMessages; import org.whispersystems.textsecuregcm.util.UrlSigner; import org.whispersystems.textsecuregcm.workers.DirectoryCommand; @@ -90,18 +94,22 @@ public class WhisperServerService extends Service { DBIFactory dbiFactory = new DBIFactory(); DBI jdbi = dbiFactory.build(environment, config.getDatabaseConfiguration(), "postgresql"); - Accounts accounts = jdbi.onDemand(Accounts.class); - PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class); - Keys keys = jdbi.onDemand(Keys.class); + Accounts accounts = jdbi.onDemand(Accounts.class); + PendingAccounts pendingAccounts = jdbi.onDemand(PendingAccounts.class); + PendingDeviceRegistrations pendingDevices = jdbi.onDemand(PendingDeviceRegistrations.class); + Keys keys = jdbi.onDemand(Keys.class); + StoredMessages storedMessages = jdbi.onDemand(StoredMessages.class); MemcachedClient memcachedClient = new MemcachedClientFactory(config.getMemcacheConfiguration()).getClient(); JedisPool redisClient = new RedisClientFactory(config.getRedisConfiguration()).getRedisClientPool(); DirectoryManager directory = new DirectoryManager(redisClient); PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, memcachedClient); + PendingDevicesManager pendingDevicesManager = new PendingDevicesManager(pendingDevices, memcachedClient); AccountsManager accountsManager = new AccountsManager(accounts, directory, memcachedClient); AccountAuthenticator accountAuthenticator = new AccountAuthenticator(accountsManager ); FederatedClientManager federatedClientManager = new FederatedClientManager(config.getFederationConfiguration()); + StoredMessageManager storedMessageManager = new StoredMessageManager(storedMessages); RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), memcachedClient); TwilioSmsSender twilioSmsSender = new TwilioSmsSender(config.getTwilioConfiguration()); Optional nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration()); @@ -109,6 +117,7 @@ public class WhisperServerService extends Service { UrlSigner urlSigner = new UrlSigner(config.getS3Configuration()); PushSender pushSender = new PushSender(config.getGcmConfiguration(), config.getApnConfiguration(), + storedMessageManager, accountsManager, directory); environment.addProvider(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()), @@ -116,10 +125,11 @@ public class WhisperServerService extends Service { accountAuthenticator, Account.class, "WhisperServer")); - environment.addResource(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender)); + environment.addResource(new AccountController(pendingAccountsManager, pendingDevicesManager, accountsManager, rateLimiters, smsSender)); environment.addResource(new DirectoryController(rateLimiters, directory)); environment.addResource(new AttachmentController(rateLimiters, federatedClientManager, urlSigner)); - environment.addResource(new KeysController(rateLimiters, keys, federatedClientManager)); + environment.addResource(new KeysController.V1(rateLimiters, keys, accountsManager, federatedClientManager)); + environment.addResource(new KeysController.V2(rateLimiters, keys, accountsManager, federatedClientManager)); environment.addResource(new FederationController(keys, accountsManager, pushSender, urlSigner)); environment.addServlet(new MessageController(rateLimiters, accountAuthenticator, diff --git a/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java b/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java index bb5bfb473..58b517de6 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java +++ b/src/main/java/org/whispersystems/textsecuregcm/auth/AccountAuthenticator.java @@ -51,7 +51,13 @@ public class AccountAuthenticator implements Authenticator authenticate(BasicCredentials basicCredentials) throws AuthenticationException { - Optional account = accountsManager.get(basicCredentials.getUsername()); + AuthorizationHeader authorizationHeader; + try { + authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword()); + } catch (InvalidAuthorizationHeaderException iahe) { + return Optional.absent(); + } + Optional account = accountsManager.get(authorizationHeader.getNumber(), authorizationHeader.getDeviceId()); if (!account.isPresent()) { return Optional.absent(); diff --git a/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationHeader.java b/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationHeader.java index d0771dd44..f102f2ea5 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationHeader.java +++ b/src/main/java/org/whispersystems/textsecuregcm/auth/AuthorizationHeader.java @@ -24,10 +24,28 @@ import java.io.IOException; public class AuthorizationHeader { - private final String user; + private final String number; + private final long accountId; private final String password; - public AuthorizationHeader(String header) throws InvalidAuthorizationHeaderException { + private AuthorizationHeader(String number, long accountId, String password) { + this.number = number; + this.accountId = accountId; + this.password = password; + } + + public static AuthorizationHeader fromUserAndPassword(String user, String password) throws InvalidAuthorizationHeaderException { + try { + String[] numberAndId = user.split("\\."); + return new AuthorizationHeader(numberAndId[0], + numberAndId.length > 1 ? Long.parseLong(numberAndId[1]) : 1, + password); + } catch (NumberFormatException nfe) { + throw new InvalidAuthorizationHeaderException(nfe); + } + } + + public static AuthorizationHeader fromFullHeader(String header) throws InvalidAuthorizationHeaderException { try { if (header == null) { throw new InvalidAuthorizationHeaderException("Null header"); @@ -55,16 +73,18 @@ public class AuthorizationHeader { throw new InvalidAuthorizationHeaderException("Badly formated credentials: " + concatenatedValues); } - this.user = credentialParts[0]; - this.password = credentialParts[1]; - + return fromUserAndPassword(credentialParts[0], credentialParts[1]); } catch (IOException ioe) { throw new InvalidAuthorizationHeaderException(ioe); } } - public String getUserName() { - return user; + public String getNumber() { + return number; + } + + public long getDeviceId() { + return accountId; } public String getPassword() { diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java index cb2ad62ff..3a6364486 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/AccountController.java @@ -16,6 +16,7 @@ */ 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; @@ -33,6 +34,7 @@ import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; +import org.whispersystems.textsecuregcm.storage.PendingDevicesManager; import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.VerificationCode; @@ -58,17 +60,20 @@ public class AccountController { private final Logger logger = LoggerFactory.getLogger(AccountController.class); - private final PendingAccountsManager pendingAccounts; - private final AccountsManager accounts; - private final RateLimiters rateLimiters; - private final SmsSender smsSender; + private final PendingAccountsManager pendingAccounts; + private final PendingDevicesManager pendingDevices; + private final AccountsManager accounts; + private final RateLimiters rateLimiters; + private final SmsSender smsSender; public AccountController(PendingAccountsManager pendingAccounts, - AccountsManager accounts, - RateLimiters rateLimiters, - SmsSender smsSenderFactory) + PendingDevicesManager pendingDevices, + AccountsManager accounts, + RateLimiters rateLimiters, + SmsSender smsSenderFactory) { this.pendingAccounts = pendingAccounts; + this.pendingDevices = pendingDevices; this.accounts = accounts; this.rateLimiters = rateLimiters; this.smsSender = smsSenderFactory; @@ -119,8 +124,8 @@ public class AccountController { throws RateLimitExceededException { try { - AuthorizationHeader header = new AuthorizationHeader(authorizationHeader); - String number = header.getUserName(); + AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader); + String number = header.getNumber(); String password = header.getPassword(); rateLimiters.getVerifyLimiter().validate(number); @@ -138,16 +143,22 @@ public class AccountController { account.setAuthenticationCredentials(new AuthenticationCredentials(password)); account.setSignalingKey(accountAttributes.getSignalingKey()); account.setSupportsSms(accountAttributes.getSupportsSms()); + account.setFetchesMessages(accountAttributes.getFetchesMessages()); + account.setDeviceId(0); + + accounts.createResetNumber(account); + + pendingAccounts.remove(number); - accounts.create(account); logger.debug("Stored account..."); - } catch (InvalidAuthorizationHeaderException e) { logger.info("Bad Authorization Header", e); throw new WebApplicationException(Response.status(401).build()); } } + + @Timed @PUT @Path("/gcm/") @@ -190,10 +201,10 @@ public class AccountController { @Produces(MediaType.APPLICATION_XML) public Response getTwiml(@PathParam("code") String encodedVerificationText) { return Response.ok().entity(String.format(TwilioSmsSender.SAY_TWIML, - encodedVerificationText)).build(); + encodedVerificationText)).build(); } - private VerificationCode generateVerificationCode() { + @VisibleForTesting protected VerificationCode generateVerificationCode() { try { SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); int randomInt = 100000 + random.nextInt(900000); @@ -203,4 +214,64 @@ public class AccountController { } } + @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/FederationController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java index e7dd510e3..bc31b4121 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/FederationController.java @@ -29,11 +29,13 @@ import org.whispersystems.textsecuregcm.entities.ClientContacts; import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import org.whispersystems.textsecuregcm.entities.PreKey; import org.whispersystems.textsecuregcm.entities.RelayMessage; +import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; import org.whispersystems.textsecuregcm.federation.FederatedPeer; import org.whispersystems.textsecuregcm.push.PushSender; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Keys; +import org.whispersystems.textsecuregcm.util.NumberData; import org.whispersystems.textsecuregcm.util.UrlSigner; import org.whispersystems.textsecuregcm.util.Util; @@ -86,16 +88,16 @@ public class FederationController { @GET @Path("/key/{number}") @Produces(MediaType.APPLICATION_JSON) - public PreKey getKey(@Auth FederatedPeer peer, + public UnstructuredPreKeyList getKey(@Auth FederatedPeer peer, @PathParam("number") String number) { - PreKey preKey = keys.get(number); + UnstructuredPreKeyList preKeys = keys.get(number, accounts.getAllByNumber(number)); - if (preKey == null) { + if (preKeys == null) { throw new WebApplicationException(Response.status(404).build()); } - return preKey; + return preKeys; } @Timed @@ -111,7 +113,7 @@ public class FederationController { .setRelay(peer.getName()) .build(); - pushSender.sendMessage(message.getDestination(), signal); + pushSender.sendMessage(message.getDestination(), message.getDestinationDeviceId(), signal); } catch (InvalidProtocolBufferException ipe) { logger.warn("ProtoBuf", ipe); throw new WebApplicationException(Response.status(400).build()); @@ -136,18 +138,15 @@ public class FederationController { public ClientContacts getUserTokens(@Auth FederatedPeer peer, @PathParam("offset") int offset) { - List accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE); + List numberList = accounts.getAllNumbers(offset, ACCOUNT_CHUNK_SIZE); List clientContacts = new LinkedList<>(); - for (Account account : accountList) { - byte[] token = Util.getContactToken(account.getNumber()); - ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms()); + for (NumberData number : numberList) { + byte[] token = Util.getContactToken(number.getNumber()); + ClientContact clientContact = new ClientContact(token, null, number.isSupportsSms()); - if (Util.isEmpty(account.getApnRegistrationId()) && - Util.isEmpty(account.getGcmRegistrationId())) - { + if (!number.isActive()) clientContact.setInactive(true); - } clientContacts.add(clientContact); } diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java index ad836afcd..6ff6fae36 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java @@ -22,10 +22,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.entities.PreKey; import org.whispersystems.textsecuregcm.entities.PreKeyList; +import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; import org.whispersystems.textsecuregcm.federation.FederatedClientManager; import org.whispersystems.textsecuregcm.federation.NoSuchPeerException; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Keys; import javax.validation.Valid; @@ -39,21 +41,24 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.List; @Path("/v1/keys") -public class KeysController { +public abstract class KeysController { private final Logger logger = LoggerFactory.getLogger(AccountController.class); private final RateLimiters rateLimiters; private final Keys keys; + private final AccountsManager accountsManager; private final FederatedClientManager federatedClientManager; - public KeysController(RateLimiters rateLimiters, Keys keys, + public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accountsManager, FederatedClientManager federatedClientManager) { this.rateLimiters = rateLimiters; this.keys = keys; + this.accountsManager = accountsManager; this.federatedClientManager = federatedClientManager; } @@ -61,32 +66,67 @@ public class KeysController { @PUT @Consumes(MediaType.APPLICATION_JSON) public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) { - keys.store(account.getNumber(), preKeys.getLastResortKey(), preKeys.getKeys()); + keys.store(account.getNumber(), account.getDeviceId(), preKeys.getLastResortKey(), preKeys.getKeys()); } - @Timed - @GET - @Path("/{number}") - @Produces(MediaType.APPLICATION_JSON) - public PreKey get(@Auth Account account, - @PathParam("number") String number, - @QueryParam("relay") String relay) - throws RateLimitExceededException + public List getKeys(Account account, String number, String relay) throws RateLimitExceededException { rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number); try { - PreKey key; + UnstructuredPreKeyList keyList; - if (relay == null) key = keys.get(number); - else key = federatedClientManager.getClient(relay).getKey(number); + if (relay == null) { + keyList = keys.get(number, accountsManager.getAllByNumber(number)); + } else { + keyList = federatedClientManager.getClient(relay).getKeys(number); + } - if (key == null) throw new WebApplicationException(Response.status(404).build()); - else return key; + if (keyList == null || keyList.getKeys().isEmpty()) throw new WebApplicationException(Response.status(404).build()); + else return keyList.getKeys(); } catch (NoSuchPeerException e) { logger.info("No peer: " + relay); throw new WebApplicationException(Response.status(404).build()); } } + @Path("/v1/keys") + public static class V1 extends KeysController { + public V1(RateLimiters rateLimiters, Keys keys, AccountsManager accountsManager, FederatedClientManager federatedClientManager) + { + super(rateLimiters, keys, accountsManager, federatedClientManager); + } + + @Timed + @GET + @Path("/{number}") + @Produces(MediaType.APPLICATION_JSON) + public PreKey get(@Auth Account account, + @PathParam("number") String number, + @QueryParam("relay") String relay) + throws RateLimitExceededException + { + return super.getKeys(account, number, relay).get(0); + } + } + + @Path("/v2/keys") + public static class V2 extends KeysController { + public V2(RateLimiters rateLimiters, Keys keys, AccountsManager accountsManager, FederatedClientManager federatedClientManager) + { + super(rateLimiters, keys, accountsManager, federatedClientManager); + } + + @Timed + @GET + @Path("/{number}") + @Produces(MediaType.APPLICATION_JSON) + public List get(@Auth Account account, + @PathParam("number") String number, + @QueryParam("relay") String relay) + throws RateLimitExceededException + { + return super.getKeys(account, number, relay); + } + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java index 4c6601a6a..ea56815eb 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java @@ -138,12 +138,13 @@ public class MessageController extends HttpServlet { try { for (Pair messagePair : listPair) { - String destination = messagePair.first().getDestination(); - String relay = messagePair.first().getRelay(); + String destination = messagePair.first().getDestination(); + long destinationDeviceId = messagePair.first().getDestinationDeviceId(); + String relay = messagePair.first().getRelay(); try { - if (Util.isEmpty(relay)) sendLocalMessage(destination, messagePair.second()); - else sendRelayMessage(relay, destination, messagePair.second()); + if (Util.isEmpty(relay)) sendLocalMessage(destination, destinationDeviceId, messagePair.second()); + else sendRelayMessage(relay, destination, destinationDeviceId, messagePair.second()); success.add(destination); } catch (NoSuchUserException e) { logger.debug("No such user", e); @@ -168,18 +169,18 @@ public class MessageController extends HttpServlet { }); } - private void sendLocalMessage(String destination, OutgoingMessageSignal outgoingMessage) + private void sendLocalMessage(String destination, long destinationDeviceId, OutgoingMessageSignal outgoingMessage) throws IOException, NoSuchUserException { - pushSender.sendMessage(destination, outgoingMessage); + pushSender.sendMessage(destination, destinationDeviceId, outgoingMessage); } - private void sendRelayMessage(String relay, String destination, OutgoingMessageSignal outgoingMessage) + private void sendRelayMessage(String relay, String destination, long destinationDeviceId, OutgoingMessageSignal outgoingMessage) throws IOException, NoSuchUserException { try { FederatedClient client = federatedClientManager.getClient(relay); - client.sendMessage(destination, outgoingMessage); + client.sendMessage(destination, destinationDeviceId, outgoingMessage); } catch (NoSuchPeerException e) { logger.info("No such peer", e); throw new NoSuchUserException(e); @@ -208,6 +209,7 @@ public class MessageController extends HttpServlet { for (IncomingMessage sub : incomingMessages) { if (sub != incoming) { + outgoingMessage.setDestinationDeviceIds(index, sub.getDestinationDeviceId()); outgoingMessage.setDestinations(index++, sub.getDestination()); } } @@ -263,8 +265,8 @@ public class MessageController extends HttpServlet { private Account authenticate(HttpServletRequest request) throws AuthenticationException { try { - AuthorizationHeader authorizationHeader = new AuthorizationHeader(request.getHeader("Authorization")); - BasicCredentials credentials = new BasicCredentials(authorizationHeader.getUserName(), + AuthorizationHeader authorizationHeader = AuthorizationHeader.fromFullHeader(request.getHeader("Authorization")); + BasicCredentials credentials = new BasicCredentials(authorizationHeader.getNumber() + "." + authorizationHeader.getDeviceId(), authorizationHeader.getPassword() ); Optional account = accountAuthenticator.authenticate(credentials); diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java b/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java index 1d1d2aa7d..18db726f9 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/AccountAttributes.java @@ -28,11 +28,15 @@ public class AccountAttributes { @JsonProperty private boolean supportsSms; + @JsonProperty + private boolean fetchesMessages; + public AccountAttributes() {} - public AccountAttributes(String signalingKey, boolean supportsSms) { + public AccountAttributes(String signalingKey, boolean supportsSms, boolean fetchesMessages) { this.signalingKey = signalingKey; this.supportsSms = supportsSms; + this.fetchesMessages = fetchesMessages; } public String getSignalingKey() { @@ -43,4 +47,8 @@ public class AccountAttributes { return supportsSms; } + public boolean getFetchesMessages() { + return fetchesMessages; + } + } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java b/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java index f5cbfb5c2..85b23a7e8 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/IncomingMessage.java @@ -38,6 +38,9 @@ public class IncomingMessage { @JsonProperty private long timestamp; + @JsonProperty + private long destinationDeviceId = 1; + public String getDestination() { return destination; } @@ -53,4 +56,12 @@ public class IncomingMessage { public String getRelay() { return relay; } + + public long getDestinationDeviceId() { + return destinationDeviceId; + } + + public void setDestinationDeviceId(long destinationDeviceId) { + this.destinationDeviceId = destinationDeviceId; + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/MessageProtos.java b/src/main/java/org/whispersystems/textsecuregcm/entities/MessageProtos.java index 60e63a559..b308aea51 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/MessageProtos.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/MessageProtos.java @@ -10,174 +10,445 @@ public final class MessageProtos { } public interface OutgoingMessageSignalOrBuilder extends com.google.protobuf.MessageOrBuilder { - + // optional uint32 type = 1; + /** + * optional uint32 type = 1; + */ boolean hasType(); + /** + * optional uint32 type = 1; + */ int getType(); - + // optional string source = 2; + /** + * optional string source = 2; + */ boolean hasSource(); - String getSource(); - + /** + * optional string source = 2; + */ + java.lang.String getSource(); + /** + * optional string source = 2; + */ + com.google.protobuf.ByteString + getSourceBytes(); + // optional string relay = 3; + /** + * optional string relay = 3; + */ boolean hasRelay(); - String getRelay(); - + /** + * optional string relay = 3; + */ + java.lang.String getRelay(); + /** + * optional string relay = 3; + */ + com.google.protobuf.ByteString + getRelayBytes(); + // repeated string destinations = 4; - java.util.List getDestinationsList(); + /** + * repeated string destinations = 4; + */ + java.util.List + getDestinationsList(); + /** + * repeated string destinations = 4; + */ int getDestinationsCount(); - String getDestinations(int index); - + /** + * repeated string destinations = 4; + */ + java.lang.String getDestinations(int index); + /** + * repeated string destinations = 4; + */ + com.google.protobuf.ByteString + getDestinationsBytes(int index); + + // repeated uint64 destinationDeviceIds = 7; + /** + * repeated uint64 destinationDeviceIds = 7; + */ + java.util.List getDestinationDeviceIdsList(); + /** + * repeated uint64 destinationDeviceIds = 7; + */ + int getDestinationDeviceIdsCount(); + /** + * repeated uint64 destinationDeviceIds = 7; + */ + long getDestinationDeviceIds(int index); + // optional uint64 timestamp = 5; + /** + * optional uint64 timestamp = 5; + */ boolean hasTimestamp(); + /** + * optional uint64 timestamp = 5; + */ long getTimestamp(); - + // optional bytes message = 6; + /** + * optional bytes message = 6; + */ boolean hasMessage(); + /** + * optional bytes message = 6; + */ com.google.protobuf.ByteString getMessage(); } + /** + * Protobuf type {@code textsecure.OutgoingMessageSignal} + */ public static final class OutgoingMessageSignal extends com.google.protobuf.GeneratedMessage implements OutgoingMessageSignalOrBuilder { // Use OutgoingMessageSignal.newBuilder() to construct. - private OutgoingMessageSignal(Builder builder) { + private OutgoingMessageSignal(com.google.protobuf.GeneratedMessage.Builder builder) { super(builder); + this.unknownFields = builder.getUnknownFields(); } - private OutgoingMessageSignal(boolean noInit) {} - + private OutgoingMessageSignal(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); } + private static final OutgoingMessageSignal defaultInstance; public static OutgoingMessageSignal getDefaultInstance() { return defaultInstance; } - + public OutgoingMessageSignal getDefaultInstanceForType() { return defaultInstance; } - + + private final com.google.protobuf.UnknownFieldSet unknownFields; + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private OutgoingMessageSignal( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + initFields(); + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + done = true; + } + break; + } + case 8: { + bitField0_ |= 0x00000001; + type_ = input.readUInt32(); + break; + } + case 18: { + bitField0_ |= 0x00000002; + source_ = input.readBytes(); + break; + } + case 26: { + bitField0_ |= 0x00000004; + relay_ = input.readBytes(); + break; + } + case 34: { + if (!((mutable_bitField0_ & 0x00000008) == 0x00000008)) { + destinations_ = new com.google.protobuf.LazyStringArrayList(); + mutable_bitField0_ |= 0x00000008; + } + destinations_.add(input.readBytes()); + break; + } + case 40: { + bitField0_ |= 0x00000008; + timestamp_ = input.readUInt64(); + break; + } + case 50: { + bitField0_ |= 0x00000010; + message_ = input.readBytes(); + break; + } + case 56: { + if (!((mutable_bitField0_ & 0x00000010) == 0x00000010)) { + destinationDeviceIds_ = new java.util.ArrayList(); + mutable_bitField0_ |= 0x00000010; + } + destinationDeviceIds_.add(input.readUInt64()); + break; + } + case 58: { + int length = input.readRawVarint32(); + int limit = input.pushLimit(length); + if (!((mutable_bitField0_ & 0x00000010) == 0x00000010) && input.getBytesUntilLimit() > 0) { + destinationDeviceIds_ = new java.util.ArrayList(); + mutable_bitField0_ |= 0x00000010; + } + while (input.getBytesUntilLimit() > 0) { + destinationDeviceIds_.add(input.readUInt64()); + } + input.popLimit(limit); + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e.getMessage()).setUnfinishedMessage(this); + } finally { + if (((mutable_bitField0_ & 0x00000008) == 0x00000008)) { + destinations_ = new com.google.protobuf.UnmodifiableLazyStringList(destinations_); + } + if (((mutable_bitField0_ & 0x00000010) == 0x00000010)) { + destinationDeviceIds_ = java.util.Collections.unmodifiableList(destinationDeviceIds_); + } + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } public static final com.google.protobuf.Descriptors.Descriptor getDescriptor() { return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor; } - + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable; + return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class, org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class); } - + + public static com.google.protobuf.Parser PARSER = + new com.google.protobuf.AbstractParser() { + public OutgoingMessageSignal parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new OutgoingMessageSignal(input, extensionRegistry); + } + }; + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + private int bitField0_; // optional uint32 type = 1; public static final int TYPE_FIELD_NUMBER = 1; private int type_; + /** + * optional uint32 type = 1; + */ public boolean hasType() { return ((bitField0_ & 0x00000001) == 0x00000001); } + /** + * optional uint32 type = 1; + */ public int getType() { return type_; } - + // optional string source = 2; public static final int SOURCE_FIELD_NUMBER = 2; private java.lang.Object source_; + /** + * optional string source = 2; + */ public boolean hasSource() { return ((bitField0_ & 0x00000002) == 0x00000002); } - public String getSource() { + /** + * optional string source = 2; + */ + public java.lang.String getSource() { java.lang.Object ref = source_; - if (ref instanceof String) { - return (String) ref; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; } else { com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - if (com.google.protobuf.Internal.isValidUtf8(bs)) { + java.lang.String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { source_ = s; } return s; } } - private com.google.protobuf.ByteString getSourceBytes() { + /** + * optional string source = 2; + */ + public com.google.protobuf.ByteString + getSourceBytes() { java.lang.Object ref = source_; - if (ref instanceof String) { + if (ref instanceof java.lang.String) { com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8((String) ref); + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); source_ = b; return b; } else { return (com.google.protobuf.ByteString) ref; } } - + // optional string relay = 3; public static final int RELAY_FIELD_NUMBER = 3; private java.lang.Object relay_; + /** + * optional string relay = 3; + */ public boolean hasRelay() { return ((bitField0_ & 0x00000004) == 0x00000004); } - public String getRelay() { + /** + * optional string relay = 3; + */ + public java.lang.String getRelay() { java.lang.Object ref = relay_; - if (ref instanceof String) { - return (String) ref; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; } else { com.google.protobuf.ByteString bs = (com.google.protobuf.ByteString) ref; - String s = bs.toStringUtf8(); - if (com.google.protobuf.Internal.isValidUtf8(bs)) { + java.lang.String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { relay_ = s; } return s; } } - private com.google.protobuf.ByteString getRelayBytes() { + /** + * optional string relay = 3; + */ + public com.google.protobuf.ByteString + getRelayBytes() { java.lang.Object ref = relay_; - if (ref instanceof String) { + if (ref instanceof java.lang.String) { com.google.protobuf.ByteString b = - com.google.protobuf.ByteString.copyFromUtf8((String) ref); + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); relay_ = b; return b; } else { return (com.google.protobuf.ByteString) ref; } } - + // repeated string destinations = 4; public static final int DESTINATIONS_FIELD_NUMBER = 4; private com.google.protobuf.LazyStringList destinations_; - public java.util.List + /** + * repeated string destinations = 4; + */ + public java.util.List getDestinationsList() { return destinations_; } + /** + * repeated string destinations = 4; + */ public int getDestinationsCount() { return destinations_.size(); } - public String getDestinations(int index) { + /** + * repeated string destinations = 4; + */ + public java.lang.String getDestinations(int index) { return destinations_.get(index); } - + /** + * repeated string destinations = 4; + */ + public com.google.protobuf.ByteString + getDestinationsBytes(int index) { + return destinations_.getByteString(index); + } + + // repeated uint64 destinationDeviceIds = 7; + public static final int DESTINATIONDEVICEIDS_FIELD_NUMBER = 7; + private java.util.List destinationDeviceIds_; + /** + * repeated uint64 destinationDeviceIds = 7; + */ + public java.util.List + getDestinationDeviceIdsList() { + return destinationDeviceIds_; + } + /** + * repeated uint64 destinationDeviceIds = 7; + */ + public int getDestinationDeviceIdsCount() { + return destinationDeviceIds_.size(); + } + /** + * repeated uint64 destinationDeviceIds = 7; + */ + public long getDestinationDeviceIds(int index) { + return destinationDeviceIds_.get(index); + } + // optional uint64 timestamp = 5; public static final int TIMESTAMP_FIELD_NUMBER = 5; private long timestamp_; + /** + * optional uint64 timestamp = 5; + */ public boolean hasTimestamp() { return ((bitField0_ & 0x00000008) == 0x00000008); } + /** + * optional uint64 timestamp = 5; + */ public long getTimestamp() { return timestamp_; } - + // optional bytes message = 6; public static final int MESSAGE_FIELD_NUMBER = 6; private com.google.protobuf.ByteString message_; + /** + * optional bytes message = 6; + */ public boolean hasMessage() { return ((bitField0_ & 0x00000010) == 0x00000010); } + /** + * optional bytes message = 6; + */ public com.google.protobuf.ByteString getMessage() { return message_; } - + private void initFields() { type_ = 0; source_ = ""; relay_ = ""; destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY; + destinationDeviceIds_ = java.util.Collections.emptyList(); timestamp_ = 0L; message_ = com.google.protobuf.ByteString.EMPTY; } @@ -185,11 +456,11 @@ public final class MessageProtos { public final boolean isInitialized() { byte isInitialized = memoizedIsInitialized; if (isInitialized != -1) return isInitialized == 1; - + memoizedIsInitialized = 1; return true; } - + public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io.IOException { getSerializedSize(); @@ -211,14 +482,17 @@ public final class MessageProtos { if (((bitField0_ & 0x00000010) == 0x00000010)) { output.writeBytes(6, message_); } + for (int i = 0; i < destinationDeviceIds_.size(); i++) { + output.writeUInt64(7, destinationDeviceIds_.get(i)); + } getUnknownFields().writeTo(output); } - + private int memoizedSerializedSize = -1; public int getSerializedSize() { int size = memoizedSerializedSize; if (size != -1) return size; - + size = 0; if (((bitField0_ & 0x00000001) == 0x00000001)) { size += com.google.protobuf.CodedOutputStream @@ -249,98 +523,96 @@ public final class MessageProtos { size += com.google.protobuf.CodedOutputStream .computeBytesSize(6, message_); } + { + int dataSize = 0; + for (int i = 0; i < destinationDeviceIds_.size(); i++) { + dataSize += com.google.protobuf.CodedOutputStream + .computeUInt64SizeNoTag(destinationDeviceIds_.get(i)); + } + size += dataSize; + size += 1 * getDestinationDeviceIdsList().size(); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; } - + private static final long serialVersionUID = 0L; @java.lang.Override protected java.lang.Object writeReplace() throws java.io.ObjectStreamException { return super.writeReplace(); } - + public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( com.google.protobuf.ByteString data) throws com.google.protobuf.InvalidProtocolBufferException { - return newBuilder().mergeFrom(data).buildParsed(); + return PARSER.parseFrom(data); } public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( com.google.protobuf.ByteString data, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws com.google.protobuf.InvalidProtocolBufferException { - return newBuilder().mergeFrom(data, extensionRegistry) - .buildParsed(); + return PARSER.parseFrom(data, extensionRegistry); } public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(byte[] data) throws com.google.protobuf.InvalidProtocolBufferException { - return newBuilder().mergeFrom(data).buildParsed(); + return PARSER.parseFrom(data); } public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( byte[] data, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws com.google.protobuf.InvalidProtocolBufferException { - return newBuilder().mergeFrom(data, extensionRegistry) - .buildParsed(); + return PARSER.parseFrom(data, extensionRegistry); } public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom(java.io.InputStream input) throws java.io.IOException { - return newBuilder().mergeFrom(input).buildParsed(); + return PARSER.parseFrom(input); } public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws java.io.IOException { - return newBuilder().mergeFrom(input, extensionRegistry) - .buildParsed(); + return PARSER.parseFrom(input, extensionRegistry); } public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseDelimitedFrom(java.io.InputStream input) throws java.io.IOException { - Builder builder = newBuilder(); - if (builder.mergeDelimitedFrom(input)) { - return builder.buildParsed(); - } else { - return null; - } + return PARSER.parseDelimitedFrom(input); } public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseDelimitedFrom( java.io.InputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws java.io.IOException { - Builder builder = newBuilder(); - if (builder.mergeDelimitedFrom(input, extensionRegistry)) { - return builder.buildParsed(); - } else { - return null; - } + return PARSER.parseDelimitedFrom(input, extensionRegistry); } public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( com.google.protobuf.CodedInputStream input) throws java.io.IOException { - return newBuilder().mergeFrom(input).buildParsed(); + return PARSER.parseFrom(input); } public static org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parseFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws java.io.IOException { - return newBuilder().mergeFrom(input, extensionRegistry) - .buildParsed(); + return PARSER.parseFrom(input, extensionRegistry); } - + public static Builder newBuilder() { return Builder.create(); } public Builder newBuilderForType() { return newBuilder(); } public static Builder newBuilder(org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal prototype) { return newBuilder().mergeFrom(prototype); } public Builder toBuilder() { return newBuilder(this); } - + @java.lang.Override protected Builder newBuilderForType( com.google.protobuf.GeneratedMessage.BuilderParent parent) { Builder builder = new Builder(parent); return builder; } + /** + * Protobuf type {@code textsecure.OutgoingMessageSignal} + */ public static final class Builder extends com.google.protobuf.GeneratedMessage.Builder implements org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignalOrBuilder { @@ -348,18 +620,21 @@ public final class MessageProtos { getDescriptor() { return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor; } - + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable internalGetFieldAccessorTable() { - return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable; + return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class, org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class); } - + // Construct using org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.newBuilder() private Builder() { maybeForceBuilderInitialization(); } - - private Builder(BuilderParent parent) { + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { super(parent); maybeForceBuilderInitialization(); } @@ -370,7 +645,7 @@ public final class MessageProtos { private static Builder create() { return new Builder(); } - + public Builder clear() { super.clear(); type_ = 0; @@ -381,26 +656,28 @@ public final class MessageProtos { bitField0_ = (bitField0_ & ~0x00000004); destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY; bitField0_ = (bitField0_ & ~0x00000008); - timestamp_ = 0L; + destinationDeviceIds_ = java.util.Collections.emptyList(); bitField0_ = (bitField0_ & ~0x00000010); - message_ = com.google.protobuf.ByteString.EMPTY; + timestamp_ = 0L; bitField0_ = (bitField0_ & ~0x00000020); + message_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000040); return this; } - + public Builder clone() { return create().mergeFrom(buildPartial()); } - + public com.google.protobuf.Descriptors.Descriptor getDescriptorForType() { - return org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDescriptor(); + return org.whispersystems.textsecuregcm.entities.MessageProtos.internal_static_textsecure_OutgoingMessageSignal_descriptor; } - + public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal getDefaultInstanceForType() { return org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDefaultInstance(); } - + public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal build() { org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = buildPartial(); if (!result.isInitialized()) { @@ -408,17 +685,7 @@ public final class MessageProtos { } return result; } - - private org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal buildParsed() - throws com.google.protobuf.InvalidProtocolBufferException { - org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = buildPartial(); - if (!result.isInitialized()) { - throw newUninitializedMessageException( - result).asInvalidProtocolBufferException(); - } - return result; - } - + public org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal buildPartial() { org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal result = new org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal(this); int from_bitField0_ = bitField0_; @@ -441,11 +708,16 @@ public final class MessageProtos { bitField0_ = (bitField0_ & ~0x00000008); } result.destinations_ = destinations_; - if (((from_bitField0_ & 0x00000010) == 0x00000010)) { + if (((bitField0_ & 0x00000010) == 0x00000010)) { + destinationDeviceIds_ = java.util.Collections.unmodifiableList(destinationDeviceIds_); + bitField0_ = (bitField0_ & ~0x00000010); + } + result.destinationDeviceIds_ = destinationDeviceIds_; + if (((from_bitField0_ & 0x00000020) == 0x00000020)) { to_bitField0_ |= 0x00000008; } result.timestamp_ = timestamp_; - if (((from_bitField0_ & 0x00000020) == 0x00000020)) { + if (((from_bitField0_ & 0x00000040) == 0x00000040)) { to_bitField0_ |= 0x00000010; } result.message_ = message_; @@ -453,7 +725,7 @@ public final class MessageProtos { onBuilt(); return result; } - + public Builder mergeFrom(com.google.protobuf.Message other) { if (other instanceof org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal) { return mergeFrom((org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal)other); @@ -462,17 +734,21 @@ public final class MessageProtos { return this; } } - + public Builder mergeFrom(org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal other) { if (other == org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.getDefaultInstance()) return this; if (other.hasType()) { setType(other.getType()); } if (other.hasSource()) { - setSource(other.getSource()); + bitField0_ |= 0x00000002; + source_ = other.source_; + onChanged(); } if (other.hasRelay()) { - setRelay(other.getRelay()); + bitField0_ |= 0x00000004; + relay_ = other.relay_; + onChanged(); } if (!other.destinations_.isEmpty()) { if (destinations_.isEmpty()) { @@ -484,6 +760,16 @@ public final class MessageProtos { } onChanged(); } + if (!other.destinationDeviceIds_.isEmpty()) { + if (destinationDeviceIds_.isEmpty()) { + destinationDeviceIds_ = other.destinationDeviceIds_; + bitField0_ = (bitField0_ & ~0x00000010); + } else { + ensureDestinationDeviceIdsIsMutable(); + destinationDeviceIds_.addAll(other.destinationDeviceIds_); + } + onChanged(); + } if (other.hasTimestamp()) { setTimestamp(other.getTimestamp()); } @@ -493,107 +779,106 @@ public final class MessageProtos { this.mergeUnknownFields(other.getUnknownFields()); return this; } - + public final boolean isInitialized() { return true; } - + public Builder mergeFrom( com.google.protobuf.CodedInputStream input, com.google.protobuf.ExtensionRegistryLite extensionRegistry) throws java.io.IOException { - com.google.protobuf.UnknownFieldSet.Builder unknownFields = - com.google.protobuf.UnknownFieldSet.newBuilder( - this.getUnknownFields()); - while (true) { - int tag = input.readTag(); - switch (tag) { - case 0: - this.setUnknownFields(unknownFields.build()); - onChanged(); - return this; - default: { - if (!parseUnknownField(input, unknownFields, - extensionRegistry, tag)) { - this.setUnknownFields(unknownFields.build()); - onChanged(); - return this; - } - break; - } - case 8: { - bitField0_ |= 0x00000001; - type_ = input.readUInt32(); - break; - } - case 18: { - bitField0_ |= 0x00000002; - source_ = input.readBytes(); - break; - } - case 26: { - bitField0_ |= 0x00000004; - relay_ = input.readBytes(); - break; - } - case 34: { - ensureDestinationsIsMutable(); - destinations_.add(input.readBytes()); - break; - } - case 40: { - bitField0_ |= 0x00000010; - timestamp_ = input.readUInt64(); - break; - } - case 50: { - bitField0_ |= 0x00000020; - message_ = input.readBytes(); - break; - } + org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal) e.getUnfinishedMessage(); + throw e; + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); } } + return this; } - private int bitField0_; - + // optional uint32 type = 1; private int type_ ; + /** + * optional uint32 type = 1; + */ public boolean hasType() { return ((bitField0_ & 0x00000001) == 0x00000001); } + /** + * optional uint32 type = 1; + */ public int getType() { return type_; } + /** + * optional uint32 type = 1; + */ public Builder setType(int value) { bitField0_ |= 0x00000001; type_ = value; onChanged(); return this; } + /** + * optional uint32 type = 1; + */ public Builder clearType() { bitField0_ = (bitField0_ & ~0x00000001); type_ = 0; onChanged(); return this; } - + // optional string source = 2; private java.lang.Object source_ = ""; + /** + * optional string source = 2; + */ public boolean hasSource() { return ((bitField0_ & 0x00000002) == 0x00000002); } - public String getSource() { + /** + * optional string source = 2; + */ + public java.lang.String getSource() { java.lang.Object ref = source_; - if (!(ref instanceof String)) { - String s = ((com.google.protobuf.ByteString) ref).toStringUtf8(); + if (!(ref instanceof java.lang.String)) { + java.lang.String s = ((com.google.protobuf.ByteString) ref) + .toStringUtf8(); source_ = s; return s; } else { - return (String) ref; + return (java.lang.String) ref; } } - public Builder setSource(String value) { + /** + * optional string source = 2; + */ + public com.google.protobuf.ByteString + getSourceBytes() { + java.lang.Object ref = source_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + source_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * optional string source = 2; + */ + public Builder setSource( + java.lang.String value) { if (value == null) { throw new NullPointerException(); } @@ -602,34 +887,72 @@ public final class MessageProtos { onChanged(); return this; } + /** + * optional string source = 2; + */ public Builder clearSource() { bitField0_ = (bitField0_ & ~0x00000002); source_ = getDefaultInstance().getSource(); onChanged(); return this; } - void setSource(com.google.protobuf.ByteString value) { - bitField0_ |= 0x00000002; + /** + * optional string source = 2; + */ + public Builder setSourceBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; source_ = value; onChanged(); + return this; } - + // optional string relay = 3; private java.lang.Object relay_ = ""; + /** + * optional string relay = 3; + */ public boolean hasRelay() { return ((bitField0_ & 0x00000004) == 0x00000004); } - public String getRelay() { + /** + * optional string relay = 3; + */ + public java.lang.String getRelay() { java.lang.Object ref = relay_; - if (!(ref instanceof String)) { - String s = ((com.google.protobuf.ByteString) ref).toStringUtf8(); + if (!(ref instanceof java.lang.String)) { + java.lang.String s = ((com.google.protobuf.ByteString) ref) + .toStringUtf8(); relay_ = s; return s; } else { - return (String) ref; + return (java.lang.String) ref; } } - public Builder setRelay(String value) { + /** + * optional string relay = 3; + */ + public com.google.protobuf.ByteString + getRelayBytes() { + java.lang.Object ref = relay_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + relay_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * optional string relay = 3; + */ + public Builder setRelay( + java.lang.String value) { if (value == null) { throw new NullPointerException(); } @@ -638,18 +961,29 @@ public final class MessageProtos { onChanged(); return this; } + /** + * optional string relay = 3; + */ public Builder clearRelay() { bitField0_ = (bitField0_ & ~0x00000004); relay_ = getDefaultInstance().getRelay(); onChanged(); return this; } - void setRelay(com.google.protobuf.ByteString value) { - bitField0_ |= 0x00000004; + /** + * optional string relay = 3; + */ + public Builder setRelayBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000004; relay_ = value; onChanged(); + return this; } - + // repeated string destinations = 4; private com.google.protobuf.LazyStringList destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY; private void ensureDestinationsIsMutable() { @@ -658,18 +992,37 @@ public final class MessageProtos { bitField0_ |= 0x00000008; } } - public java.util.List + /** + * repeated string destinations = 4; + */ + public java.util.List getDestinationsList() { return java.util.Collections.unmodifiableList(destinations_); } + /** + * repeated string destinations = 4; + */ public int getDestinationsCount() { return destinations_.size(); } - public String getDestinations(int index) { + /** + * repeated string destinations = 4; + */ + public java.lang.String getDestinations(int index) { return destinations_.get(index); } + /** + * repeated string destinations = 4; + */ + public com.google.protobuf.ByteString + getDestinationsBytes(int index) { + return destinations_.getByteString(index); + } + /** + * repeated string destinations = 4; + */ public Builder setDestinations( - int index, String value) { + int index, java.lang.String value) { if (value == null) { throw new NullPointerException(); } @@ -678,7 +1031,11 @@ public final class MessageProtos { onChanged(); return this; } - public Builder addDestinations(String value) { + /** + * repeated string destinations = 4; + */ + public Builder addDestinations( + java.lang.String value) { if (value == null) { throw new NullPointerException(); } @@ -687,87 +1044,191 @@ public final class MessageProtos { onChanged(); return this; } + /** + * repeated string destinations = 4; + */ public Builder addAllDestinations( - java.lang.Iterable values) { + java.lang.Iterable values) { ensureDestinationsIsMutable(); super.addAll(values, destinations_); onChanged(); return this; } + /** + * repeated string destinations = 4; + */ public Builder clearDestinations() { destinations_ = com.google.protobuf.LazyStringArrayList.EMPTY; bitField0_ = (bitField0_ & ~0x00000008); onChanged(); return this; } - void addDestinations(com.google.protobuf.ByteString value) { - ensureDestinationsIsMutable(); + /** + * repeated string destinations = 4; + */ + public Builder addDestinationsBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + ensureDestinationsIsMutable(); destinations_.add(value); onChanged(); + return this; } - + + // repeated uint64 destinationDeviceIds = 7; + private java.util.List destinationDeviceIds_ = java.util.Collections.emptyList(); + private void ensureDestinationDeviceIdsIsMutable() { + if (!((bitField0_ & 0x00000010) == 0x00000010)) { + destinationDeviceIds_ = new java.util.ArrayList(destinationDeviceIds_); + bitField0_ |= 0x00000010; + } + } + /** + * repeated uint64 destinationDeviceIds = 7; + */ + public java.util.List + getDestinationDeviceIdsList() { + return java.util.Collections.unmodifiableList(destinationDeviceIds_); + } + /** + * repeated uint64 destinationDeviceIds = 7; + */ + public int getDestinationDeviceIdsCount() { + return destinationDeviceIds_.size(); + } + /** + * repeated uint64 destinationDeviceIds = 7; + */ + public long getDestinationDeviceIds(int index) { + return destinationDeviceIds_.get(index); + } + /** + * repeated uint64 destinationDeviceIds = 7; + */ + public Builder setDestinationDeviceIds( + int index, long value) { + ensureDestinationDeviceIdsIsMutable(); + destinationDeviceIds_.set(index, value); + onChanged(); + return this; + } + /** + * repeated uint64 destinationDeviceIds = 7; + */ + public Builder addDestinationDeviceIds(long value) { + ensureDestinationDeviceIdsIsMutable(); + destinationDeviceIds_.add(value); + onChanged(); + return this; + } + /** + * repeated uint64 destinationDeviceIds = 7; + */ + public Builder addAllDestinationDeviceIds( + java.lang.Iterable values) { + ensureDestinationDeviceIdsIsMutable(); + super.addAll(values, destinationDeviceIds_); + onChanged(); + return this; + } + /** + * repeated uint64 destinationDeviceIds = 7; + */ + public Builder clearDestinationDeviceIds() { + destinationDeviceIds_ = java.util.Collections.emptyList(); + bitField0_ = (bitField0_ & ~0x00000010); + onChanged(); + return this; + } + // optional uint64 timestamp = 5; private long timestamp_ ; + /** + * optional uint64 timestamp = 5; + */ public boolean hasTimestamp() { - return ((bitField0_ & 0x00000010) == 0x00000010); + return ((bitField0_ & 0x00000020) == 0x00000020); } + /** + * optional uint64 timestamp = 5; + */ public long getTimestamp() { return timestamp_; } + /** + * optional uint64 timestamp = 5; + */ public Builder setTimestamp(long value) { - bitField0_ |= 0x00000010; + bitField0_ |= 0x00000020; timestamp_ = value; onChanged(); return this; } + /** + * optional uint64 timestamp = 5; + */ public Builder clearTimestamp() { - bitField0_ = (bitField0_ & ~0x00000010); + bitField0_ = (bitField0_ & ~0x00000020); timestamp_ = 0L; onChanged(); return this; } - + // optional bytes message = 6; private com.google.protobuf.ByteString message_ = com.google.protobuf.ByteString.EMPTY; + /** + * optional bytes message = 6; + */ public boolean hasMessage() { - return ((bitField0_ & 0x00000020) == 0x00000020); + return ((bitField0_ & 0x00000040) == 0x00000040); } + /** + * optional bytes message = 6; + */ public com.google.protobuf.ByteString getMessage() { return message_; } + /** + * optional bytes message = 6; + */ public Builder setMessage(com.google.protobuf.ByteString value) { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00000020; + bitField0_ |= 0x00000040; message_ = value; onChanged(); return this; } + /** + * optional bytes message = 6; + */ public Builder clearMessage() { - bitField0_ = (bitField0_ & ~0x00000020); + bitField0_ = (bitField0_ & ~0x00000040); message_ = getDefaultInstance().getMessage(); onChanged(); return this; } - + // @@protoc_insertion_point(builder_scope:textsecure.OutgoingMessageSignal) } - + static { defaultInstance = new OutgoingMessageSignal(true); defaultInstance.initFields(); } - + // @@protoc_insertion_point(class_scope:textsecure.OutgoingMessageSignal) } - + private static com.google.protobuf.Descriptors.Descriptor internal_static_textsecure_OutgoingMessageSignal_descriptor; private static com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable; - + public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { return descriptor; @@ -777,11 +1238,12 @@ public final class MessageProtos { static { java.lang.String[] descriptorData = { "\n\033OutgoingMessageSignal.proto\022\ntextsecur" + - "e\"~\n\025OutgoingMessageSignal\022\014\n\004type\030\001 \001(\r" + - "\022\016\n\006source\030\002 \001(\t\022\r\n\005relay\030\003 \001(\t\022\024\n\014desti" + - "nations\030\004 \003(\t\022\021\n\ttimestamp\030\005 \001(\004\022\017\n\007mess" + - "age\030\006 \001(\014B:\n)org.whispersystems.textsecu" + - "regcm.entitiesB\rMessageProtos" + "e\"\234\001\n\025OutgoingMessageSignal\022\014\n\004type\030\001 \001(" + + "\r\022\016\n\006source\030\002 \001(\t\022\r\n\005relay\030\003 \001(\t\022\024\n\014dest" + + "inations\030\004 \003(\t\022\034\n\024destinationDeviceIds\030\007" + + " \003(\004\022\021\n\ttimestamp\030\005 \001(\004\022\017\n\007message\030\006 \001(\014" + + "B:\n)org.whispersystems.textsecuregcm.ent" + + "itiesB\rMessageProtos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -793,9 +1255,7 @@ public final class MessageProtos { internal_static_textsecure_OutgoingMessageSignal_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_textsecure_OutgoingMessageSignal_descriptor, - new java.lang.String[] { "Type", "Source", "Relay", "Destinations", "Timestamp", "Message", }, - org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.class, - org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal.Builder.class); + new java.lang.String[] { "Type", "Source", "Relay", "Destinations", "DestinationDeviceIds", "Timestamp", "Message", }); return null; } }; @@ -804,6 +1264,6 @@ public final class MessageProtos { new com.google.protobuf.Descriptors.FileDescriptor[] { }, assigner); } - + // @@protoc_insertion_point(outer_class_scope) } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java index 69a0cae56..7c589b4fc 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java @@ -34,6 +34,10 @@ public class PreKey { @JsonIgnore private String number; + @JsonProperty + @NotNull + private long deviceId; + @JsonProperty @NotNull private long keyId; @@ -51,12 +55,13 @@ public class PreKey { public PreKey() {} - public PreKey(long id, String number, long keyId, + public PreKey(long id, String number, long deviceId, long keyId, String publicKey, String identityKey, boolean lastResort) { this.id = id; this.number = number; + this.deviceId = deviceId; this.keyId = keyId; this.publicKey = publicKey; this.identityKey = identityKey; @@ -113,4 +118,12 @@ public class PreKey { public void setLastResort(boolean lastResort) { this.lastResort = lastResort; } + + public void setDeviceId(long deviceId) { + this.deviceId = deviceId; + } + + public long getDeviceId() { + return deviceId; + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/RelayMessage.java b/src/main/java/org/whispersystems/textsecuregcm/entities/RelayMessage.java index 4a8ccb2f8..411eb5d8f 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/RelayMessage.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/RelayMessage.java @@ -32,6 +32,10 @@ public class RelayMessage { @NotEmpty private String destination; + @JsonProperty + @NotEmpty + private long destinationDeviceId; + @JsonProperty @NotNull @JsonSerialize(using = ByteArrayAdapter.Serializing.class) @@ -40,7 +44,7 @@ public class RelayMessage { public RelayMessage() {} - public RelayMessage(String destination, byte[] outgoingMessageSignal) { + public RelayMessage(String destination, long destinationDeviceId, byte[] outgoingMessageSignal) { this.destination = destination; this.outgoingMessageSignal = outgoingMessageSignal; } @@ -49,6 +53,10 @@ public class RelayMessage { return destination; } + public long getDestinationDeviceId() { + return destinationDeviceId; + } + public byte[] getOutgoingMessageSignal() { return outgoingMessageSignal; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/UnstructuredPreKeyList.java b/src/main/java/org/whispersystems/textsecuregcm/entities/UnstructuredPreKeyList.java new file mode 100644 index 000000000..016d431cd --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/UnstructuredPreKeyList.java @@ -0,0 +1,53 @@ +/** + * 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.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.Iterator; +import java.util.List; + +public class UnstructuredPreKeyList { + @JsonProperty + @NotNull + @Valid + private List keys; + + public UnstructuredPreKeyList(List preKeys) { + this.keys = preKeys; + } + + public List getKeys() { + return keys; + } + + @VisibleForTesting public boolean equals(Object o) { + if (!(o instanceof UnstructuredPreKeyList) || + ((UnstructuredPreKeyList) o).keys.size() != keys.size()) + return false; + Iterator otherKeys = ((UnstructuredPreKeyList) o).keys.iterator(); + for (PreKey key : keys) { + if (!otherKeys.next().equals(key)) + return false; + } + return true; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java b/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java index 19a75a132..a008e43e3 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java +++ b/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java @@ -38,6 +38,7 @@ import org.whispersystems.textsecuregcm.entities.ClientContacts; import org.whispersystems.textsecuregcm.entities.MessageProtos.OutgoingMessageSignal; import org.whispersystems.textsecuregcm.entities.PreKey; import org.whispersystems.textsecuregcm.entities.RelayMessage; +import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; import org.whispersystems.textsecuregcm.util.Base64; import javax.net.ssl.SSLContext; @@ -99,12 +100,12 @@ public class FederatedClient { } } - public PreKey getKey(String destination) { + public UnstructuredPreKeyList getKeys(String destination) { try { WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH, destination)); return resource.accept(MediaType.APPLICATION_JSON) .header("Authorization", authorizationHeader) - .get(PreKey.class); + .get(UnstructuredPreKeyList.class); } catch (UniformInterfaceException | ClientHandlerException e) { logger.warn("PreKey", e); return null; @@ -139,14 +140,14 @@ public class FederatedClient { } } - public void sendMessage(String destination, OutgoingMessageSignal message) + public void sendMessage(String destination, long destinationDeviceId, OutgoingMessageSignal message) throws IOException, NoSuchUserException { try { WebResource resource = client.resource(peer.getUrl()).path(RELAY_MESSAGE_PATH); ClientResponse response = resource.type(MediaType.APPLICATION_JSON) .header("Authorization", authorizationHeader) - .entity(new RelayMessage(destination, message.toByteArray())) + .entity(new RelayMessage(destination, destinationDeviceId, message.toByteArray())) .put(ClientResponse.class); if (response.getStatus() == 404) { diff --git a/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java b/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java index a440f6c0d..f0054baf2 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java +++ b/src/main/java/org/whispersystems/textsecuregcm/push/PushSender.java @@ -27,11 +27,13 @@ import org.whispersystems.textsecuregcm.entities.MessageProtos; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.DirectoryManager; +import org.whispersystems.textsecuregcm.storage.StoredMessageManager; import java.io.IOException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import java.util.List; public class PushSender { @@ -42,9 +44,11 @@ public class PushSender { private final GCMSender gcmSender; private final APNSender apnSender; + private final StoredMessageManager storedMessageManager; public PushSender(GcmConfiguration gcmConfiguration, ApnConfiguration apnConfiguration, + StoredMessageManager storedMessageManager, AccountsManager accounts, DirectoryManager directory) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException @@ -52,25 +56,27 @@ public class PushSender { this.accounts = accounts; this.directory = directory; - this.gcmSender = new GCMSender(gcmConfiguration.getApiKey()); - this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey()); + this.storedMessageManager = storedMessageManager; + this.gcmSender = new GCMSender(gcmConfiguration.getApiKey()); + this.apnSender = new APNSender(apnConfiguration.getCertificate(), apnConfiguration.getKey()); } - public void sendMessage(String destination, MessageProtos.OutgoingMessageSignal outgoingMessage) + public void sendMessage(String destination, long destinationDeviceId, MessageProtos.OutgoingMessageSignal outgoingMessage) throws IOException, NoSuchUserException { - Optional account = accounts.get(destination); + Optional accountOptional = accounts.get(destination, destinationDeviceId); - if (!account.isPresent()) { - directory.remove(destination); + if (!accountOptional.isPresent()) { throw new NoSuchUserException("No such local destination: " + destination); } + Account account = accountOptional.get(); - String signalingKey = account.get().getSignalingKey(); + String signalingKey = account.getSignalingKey(); EncryptedOutgoingMessage message = new EncryptedOutgoingMessage(outgoingMessage, signalingKey); - if (account.get().getGcmRegistrationId() != null) sendGcmMessage(account.get(), message); - else if (account.get().getApnRegistrationId() != null) sendApnMessage(account.get(), message); + if (account.getGcmRegistrationId() != null) sendGcmMessage(account, message); + else if (account.getApnRegistrationId() != null) sendApnMessage(account, message); + else if (account.getFetchesMessages()) storeFetchedMessage(account, message); else throw new NoSuchUserException("No push identifier!"); } @@ -100,4 +106,7 @@ public class PushSender { apnSender.sendMessage(account.getApnRegistrationId(), outgoingMessage); } + private void storeFetchedMessage(Account account, EncryptedOutgoingMessage outgoingMessage) { + storedMessageManager.storeMessage(account, outgoingMessage); + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index 41ff1c428..f16bc1d23 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -27,27 +27,36 @@ public class Account implements Serializable { private long id; private String number; + private long deviceId; private String hashedAuthenticationToken; private String salt; private String signalingKey; + /** + * In order for us to tell a client that an account is "inactive" (ie go use SMS for transport), we check that all + * non-fetching Accounts don't have push registrations. In this way, we can ensure that we have some form of transport + * available for all Accounts on all "active" numbers. + */ private String gcmRegistrationId; private String apnRegistrationId; private boolean supportsSms; + private boolean fetchesMessages; public Account() {} - public Account(long id, String number, String hashedAuthenticationToken, String salt, + public Account(long id, String number, long deviceId, String hashedAuthenticationToken, String salt, String signalingKey, String gcmRegistrationId, String apnRegistrationId, - boolean supportsSms) + boolean supportsSms, boolean fetchesMessages) { this.id = id; this.number = number; + this.deviceId = deviceId; this.hashedAuthenticationToken = hashedAuthenticationToken; this.salt = salt; this.signalingKey = signalingKey; this.gcmRegistrationId = gcmRegistrationId; this.apnRegistrationId = apnRegistrationId; this.supportsSms = supportsSms; + this.fetchesMessages = fetchesMessages; } public String getApnRegistrationId() { @@ -74,6 +83,14 @@ public class Account implements Serializable { return number; } + public long getDeviceId() { + return deviceId; + } + + public void setDeviceId(long deviceId) { + this.deviceId = deviceId; + } + public void setAuthenticationCredentials(AuthenticationCredentials credentials) { this.hashedAuthenticationToken = credentials.getHashedAuthenticationToken(); this.salt = credentials.getSalt(); @@ -106,4 +123,12 @@ public class Account implements Serializable { public void setId(long id) { this.id = id; } + + public void setFetchesMessages(boolean fetchesMessages) { + this.fetchesMessages = fetchesMessages; + } + + public boolean getFetchesMessages() { + return fetchesMessages; + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java index ba941846f..3ace80c17 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Accounts.java @@ -29,6 +29,7 @@ import org.skife.jdbi.v2.sqlobject.SqlUpdate; import org.skife.jdbi.v2.sqlobject.Transaction; import org.skife.jdbi.v2.sqlobject.customizers.Mapper; import org.skife.jdbi.v2.tweak.ResultSetMapper; +import org.whispersystems.textsecuregcm.util.NumberData; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; @@ -42,50 +43,76 @@ import java.util.List; public abstract class Accounts { - public static final String ID = "id"; - public static final String NUMBER = "number"; - public static final String AUTH_TOKEN = "auth_token"; - public static final String SALT = "salt"; - public static final String SIGNALING_KEY = "signaling_key"; - public static final String GCM_ID = "gcm_id"; - public static final String APN_ID = "apn_id"; - public static final String SUPPORTS_SMS = "supports_sms"; + public static final String ID = "id"; + public static final String NUMBER = "number"; + public static final String DEVICE_ID = "device_id"; + public static final String AUTH_TOKEN = "auth_token"; + public static final String SALT = "salt"; + public static final String SIGNALING_KEY = "signaling_key"; + public static final String GCM_ID = "gcm_id"; + public static final String APN_ID = "apn_id"; + public static final String FETCHES_MESSAGES = "fetches_messages"; + public static final String SUPPORTS_SMS = "supports_sms"; - @SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + AUTH_TOKEN + ", " + - SALT + ", " + SIGNALING_KEY + ", " + GCM_ID + ", " + - APN_ID + ", " + SUPPORTS_SMS + ") " + - "VALUES (:number, :auth_token, :salt, :signaling_key, :gcm_id, :apn_id, :supports_sms)") + @SqlUpdate("INSERT INTO accounts (" + NUMBER + ", " + DEVICE_ID + ", " + AUTH_TOKEN + ", " + + SALT + ", " + SIGNALING_KEY + ", " + FETCHES_MESSAGES + ", " + + GCM_ID + ", " + APN_ID + ", " + SUPPORTS_SMS + ") " + + "VALUES (:number, :device_id, :auth_token, :salt, :signaling_key, :fetches_messages, :gcm_id, :apn_id, :supports_sms)") @GetGeneratedKeys - abstract long createStep(@AccountBinder Account account); + abstract long insertStep(@AccountBinder Account account); - @SqlUpdate("DELETE FROM accounts WHERE number = :number") - abstract void removeStep(@Bind("number") String number); + @SqlQuery("SELECT " + DEVICE_ID + " FROM accounts WHERE " + NUMBER + " = :number ORDER BY " + DEVICE_ID + " DESC LIMIT 1 FOR UPDATE") + abstract long getHighestDeviceId(@Bind("number") String number); + + @Transaction(TransactionIsolationLevel.SERIALIZABLE) + public long insert(@AccountBinder Account account) { + account.setDeviceId(getHighestDeviceId(account.getNumber()) + 1); + return insertStep(account); + } + + @SqlUpdate("DELETE FROM accounts WHERE " + NUMBER + " = :number RETURNING id") + abstract void removeAccountsByNumber(@Bind("number") String number); @SqlUpdate("UPDATE accounts SET " + AUTH_TOKEN + " = :auth_token, " + SALT + " = :salt, " + - SIGNALING_KEY + " = :signaling_key, " + GCM_ID + " = :gcm_id, " + - APN_ID + " = :apn_id, " + SUPPORTS_SMS + " = :supports_sms " + - "WHERE " + NUMBER + " = :number") + SIGNALING_KEY + " = :signaling_key, " + GCM_ID + " = :gcm_id, " + APN_ID + " = :apn_id, " + + FETCHES_MESSAGES + " = :fetches_messages, " + SUPPORTS_SMS + " = :supports_sms " + + "WHERE " + NUMBER + " = :number AND " + DEVICE_ID + " = :device_id") abstract void update(@AccountBinder Account account); + @Mapper(AccountMapper.class) + @SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number AND " + DEVICE_ID + " = :device_id") + abstract Account get(@Bind("number") String number, @Bind("device_id") long deviceId); + + @SqlQuery("SELECT COUNT(DISTINCT " + NUMBER + ") from accounts") + abstract long getNumberCount(); + + private static final String NUMBER_DATA_QUERY = "SELECT number, COUNT(" + + "CASE WHEN (" + GCM_ID + " IS NOT NULL OR " + APN_ID + " IS NOT NULL OR " + FETCHES_MESSAGES + " = 1) " + + "THEN 1 ELSE 0 END) AS active, COUNT(" + + "CASE WHEN " + SUPPORTS_SMS + " = 1 THEN 1 ELSE 0 END) AS " + SUPPORTS_SMS + " " + + "FROM accounts"; + + @Mapper(NumberDataMapper.class) + @SqlQuery(NUMBER_DATA_QUERY + " GROUP BY " + NUMBER + " OFFSET :offset LIMIT :limit") + abstract List getAllNumbers(@Bind("offset") int offset, @Bind("limit") int length); + + @Mapper(NumberDataMapper.class) + @SqlQuery(NUMBER_DATA_QUERY + " GROUP BY " + NUMBER) + public abstract Iterator getAllNumbers(); + + @Mapper(NumberDataMapper.class) + @SqlQuery(NUMBER_DATA_QUERY + " WHERE " + NUMBER + " = :number GROUP BY " + NUMBER) + abstract NumberData getNumberData(@Bind("number") String number); + @Mapper(AccountMapper.class) @SqlQuery("SELECT * FROM accounts WHERE " + NUMBER + " = :number") - abstract Account get(@Bind("number") String number); + public abstract List getAllByNumber(@Bind("number") String number); - @SqlQuery("SELECT COUNT(*) from accounts") - abstract long getCount(); - - @Mapper(AccountMapper.class) - @SqlQuery("SELECT * FROM accounts OFFSET :offset LIMIT :limit") - abstract List getAll(@Bind("offset") int offset, @Bind("limit") int length); - - @Mapper(AccountMapper.class) - @SqlQuery("SELECT * FROM accounts") - abstract Iterator getAll(); - - @Transaction(TransactionIsolationLevel.REPEATABLE_READ) - public long create(Account account) { - removeStep(account.getNumber()); - return createStep(account); + @Transaction(TransactionIsolationLevel.SERIALIZABLE) + public long insertClearingNumber(Account account) { + removeAccountsByNumber(account.getNumber()); + account.setDeviceId(getHighestDeviceId(account.getNumber()) + 1); + return insertStep(account); } public static class AccountMapper implements ResultSetMapper { @@ -94,11 +121,21 @@ public abstract class Accounts { public Account map(int i, ResultSet resultSet, StatementContext statementContext) throws SQLException { - return new Account(resultSet.getLong(ID), resultSet.getString(NUMBER), + return new Account(resultSet.getLong(ID), resultSet.getString(NUMBER), resultSet.getLong(DEVICE_ID), resultSet.getString(AUTH_TOKEN), resultSet.getString(SALT), resultSet.getString(SIGNALING_KEY), resultSet.getString(GCM_ID), resultSet.getString(APN_ID), - resultSet.getInt(SUPPORTS_SMS) == 1); + resultSet.getInt(SUPPORTS_SMS) == 1, resultSet.getInt(FETCHES_MESSAGES) == 1); + } + } + + public static class NumberDataMapper implements ResultSetMapper { + + @Override + public NumberData map(int i, ResultSet resultSet, StatementContext statementContext) + throws SQLException + { + return new NumberData(resultSet.getString("number"), resultSet.getInt("active") != 0, resultSet.getInt(SUPPORTS_SMS) != 0); } } @@ -117,6 +154,7 @@ public abstract class Accounts { { sql.bind(ID, account.getId()); sql.bind(NUMBER, account.getNumber()); + sql.bind(DEVICE_ID, account.getDeviceId()); sql.bind(AUTH_TOKEN, account.getAuthenticationCredentials() .getHashedAuthenticationToken()); sql.bind(SALT, account.getAuthenticationCredentials().getSalt()); @@ -124,6 +162,7 @@ public abstract class Accounts { sql.bind(GCM_ID, account.getGcmRegistrationId()); sql.bind(APN_ID, account.getApnRegistrationId()); sql.bind(SUPPORTS_SMS, account.getSupportsSms() ? 1 : 0); + sql.bind(FETCHES_MESSAGES, account.getFetchesMessages() ? 1 : 0); } }; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java b/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java index adfc87285..19ec7d78b 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java @@ -20,6 +20,7 @@ package org.whispersystems.textsecuregcm.storage; import com.google.common.base.Optional; import net.spy.memcached.MemcachedClient; import org.whispersystems.textsecuregcm.entities.ClientContact; +import org.whispersystems.textsecuregcm.util.NumberData; import org.whispersystems.textsecuregcm.util.Util; import java.util.Iterator; @@ -41,24 +42,36 @@ public class AccountsManager { } public long getCount() { - return accounts.getCount(); + return accounts.getNumberCount(); } - public List getAll(int offset, int length) { - return accounts.getAll(offset, length); + public List getAllNumbers(int offset, int length) { + return accounts.getAllNumbers(offset, length); } - public Iterator getAll() { - return accounts.getAll(); + public Iterator getAllNumbers() { + return accounts.getAllNumbers(); } - public void create(Account account) { - long id = accounts.create(account); - + /** Creates a new Account and NumberData, clearing all existing accounts/data on the given number */ + public void createResetNumber(Account account) { + long id = accounts.insertClearingNumber(account); account.setId(id); if (memcachedClient != null) { - memcachedClient.set(getKey(account.getNumber()), 0, account); + memcachedClient.set(getKey(account.getNumber(), account.getDeviceId()), 0, account); + } + + updateDirectory(account); + } + + /** Creates a new Account for an existing NumberData (setting the deviceId) */ + public void createAccountOnExistingNumber(Account account) { + long id = accounts.insert(account); + account.setId(id); + + if (memcachedClient != null) { + memcachedClient.set(getKey(account.getNumber(), account.getDeviceId()), 0, account); } updateDirectory(account); @@ -66,25 +79,25 @@ public class AccountsManager { public void update(Account account) { if (memcachedClient != null) { - memcachedClient.set(getKey(account.getNumber()), 0, account); + memcachedClient.set(getKey(account.getNumber(), account.getDeviceId()), 0, account); } accounts.update(account); updateDirectory(account); } - public Optional get(String number) { + public Optional get(String number, long deviceId) { Account account = null; if (memcachedClient != null) { - account = (Account)memcachedClient.get(getKey(number)); + account = (Account)memcachedClient.get(getKey(number, deviceId)); } if (account == null) { - account = accounts.get(number); + account = accounts.get(number, deviceId); if (account != null && memcachedClient != null) { - memcachedClient.set(getKey(number), 0, account); + memcachedClient.set(getKey(number, deviceId), 0, account); } } @@ -92,17 +105,31 @@ public class AccountsManager { else return Optional.absent(); } + public List getAllByNumber(String number) { + return accounts.getAllByNumber(number); + } + private void updateDirectory(Account account) { - if (account.getGcmRegistrationId() != null || account.getApnRegistrationId() != null) { + boolean active = account.getFetchesMessages() || + !Util.isEmpty(account.getApnRegistrationId()) || !Util.isEmpty(account.getGcmRegistrationId()); + boolean supportsSms = account.getSupportsSms(); + + if (!active || !supportsSms) { + NumberData numberData = accounts.getNumberData(account.getNumber()); + active = numberData.isActive(); + supportsSms = numberData.isSupportsSms(); + } + + if (active) { byte[] token = Util.getContactToken(account.getNumber()); - ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms()); + ClientContact clientContact = new ClientContact(token, null, supportsSms); directory.add(clientContact); } else { directory.remove(account.getNumber()); } } - private String getKey(String number) { - return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number; + private String getKey(String number, long accountId) { + return Account.class.getSimpleName() + Account.MEMCACHE_VERION + number + accountId; } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java index 16cf88d2b..772a66eab 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java @@ -30,6 +30,7 @@ import org.skife.jdbi.v2.sqlobject.Transaction; import org.skife.jdbi.v2.sqlobject.customizers.Mapper; import org.skife.jdbi.v2.tweak.ResultSetMapper; import org.whispersystems.textsecuregcm.entities.PreKey; +import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; @@ -38,48 +39,60 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.LinkedList; import java.util.List; public abstract class Keys { - @SqlUpdate("DELETE FROM keys WHERE number = :number") - abstract void removeKeys(@Bind("number") String number); + @SqlUpdate("DELETE FROM keys WHERE number = :number AND device_id = :device_id") + abstract void removeKeys(@Bind("number") String number, @Bind("device_id") long deviceId); @SqlUpdate("DELETE FROM keys WHERE id = :id") abstract void removeKey(@Bind("id") long id); - @SqlBatch("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)") + @SqlBatch("INSERT INTO keys (number, device_id, key_id, public_key, identity_key, last_resort) VALUES " + + "(:number, :device_id, :key_id, :public_key, :identity_key, :last_resort)") abstract void append(@PreKeyBinder List preKeys); - @SqlUpdate("INSERT INTO keys (number, key_id, public_key, identity_key, last_resort) VALUES (:number, :key_id, :public_key, :identity_key, :last_resort)") + @SqlUpdate("INSERT INTO keys (number, device_id, key_id, public_key, identity_key, last_resort) VALUES " + + "(:number, :device_id, :key_id, :public_key, :identity_key, :last_resort)") abstract void append(@PreKeyBinder PreKey preKey); - @SqlQuery("SELECT * FROM keys WHERE number = :number ORDER BY id LIMIT 1 FOR UPDATE") + @SqlQuery("SELECT * FROM keys WHERE number = :number AND device_id = :device_id ORDER BY key_id ASC FOR UPDATE") @Mapper(PreKeyMapper.class) - abstract PreKey retrieveFirst(@Bind("number") String number); + abstract PreKey retrieveFirst(@Bind("number") String number, @Bind("device_id") long deviceId); @Transaction(TransactionIsolationLevel.SERIALIZABLE) - public void store(String number, PreKey lastResortKey, List keys) { + public void store(String number, long deviceId, PreKey lastResortKey, List keys) { for (PreKey key : keys) { key.setNumber(number); + key.setDeviceId(deviceId); } lastResortKey.setNumber(number); + lastResortKey.setDeviceId(deviceId); + lastResortKey.setLastResort(true); - removeKeys(number); + removeKeys(number, deviceId); append(keys); append(lastResortKey); } @Transaction(TransactionIsolationLevel.SERIALIZABLE) - public PreKey get(String number) { - PreKey preKey = retrieveFirst(number); - - if (preKey != null && !preKey.isLastResort()) { - removeKey(preKey.getId()); + public UnstructuredPreKeyList get(String number, List accounts) { + List preKeys = new LinkedList<>(); + for (Account account : accounts) { + PreKey preKey = retrieveFirst(number, account.getDeviceId()); + if (preKey != null) + preKeys.add(preKey); } - return preKey; + for (PreKey preKey : preKeys) { + if (!preKey.isLastResort()) + removeKey(preKey.getId()); + } + + return new UnstructuredPreKeyList(preKeys); } @BindingAnnotation(PreKeyBinder.PreKeyBinderFactory.class) @@ -95,6 +108,7 @@ public abstract class Keys { { sql.bind("id", preKey.getId()); sql.bind("number", preKey.getNumber()); + sql.bind("device_id", preKey.getDeviceId()); sql.bind("key_id", preKey.getKeyId()); sql.bind("public_key", preKey.getPublicKey()); sql.bind("identity_key", preKey.getIdentityKey()); @@ -111,7 +125,7 @@ public abstract class Keys { public PreKey map(int i, ResultSet resultSet, StatementContext statementContext) throws SQLException { - return new PreKey(resultSet.getLong("id"), resultSet.getString("number"), + return new PreKey(resultSet.getLong("id"), resultSet.getString("number"), resultSet.getLong("device_id"), resultSet.getLong("key_id"), resultSet.getString("public_key"), resultSet.getString("identity_key"), resultSet.getInt("last_resort") == 1); diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java index f54c165e7..bd1a374e5 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccounts.java @@ -29,4 +29,6 @@ public interface PendingAccounts { @SqlQuery("SELECT verification_code FROM pending_accounts WHERE number = :number") String getCodeForNumber(@Bind("number") String number); + @SqlUpdate("DELETE FROM pending_accounts WHERE number = :number") + void remove(@Bind("number") String number); } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java index 306fda280..a6eddf026 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingAccountsManager.java @@ -41,6 +41,12 @@ public class PendingAccountsManager { pendingAccounts.insert(number, code); } + public void remove(String number) { + if (memcachedClient != null) + memcachedClient.delete(MEMCACHE_PREFIX + number); + pendingAccounts.remove(number); + } + public Optional getCodeForNumber(String number) { String code = null; diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDeviceRegistrations.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDeviceRegistrations.java new file mode 100644 index 000000000..1db4db6cb --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDeviceRegistrations.java @@ -0,0 +1,34 @@ +/** + * 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.storage; + +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; + +public interface PendingDeviceRegistrations { + + @SqlUpdate("WITH upsert AS (UPDATE pending_devices SET verification_code = :verification_code WHERE number = :number RETURNING *) " + + "INSERT INTO pending_devices (number, verification_code) SELECT :number, :verification_code WHERE NOT EXISTS (SELECT * FROM upsert)") + void insert(@Bind("number") String number, @Bind("verification_code") String verificationCode); + + @SqlQuery("SELECT verification_code FROM pending_devices WHERE number = :number") + String getCodeForNumber(@Bind("number") String number); + + @SqlUpdate("DELETE FROM pending_devices WHERE number = :number") + void remove(@Bind("number") String number); +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java new file mode 100644 index 000000000..d67924c16 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/PendingDevicesManager.java @@ -0,0 +1,68 @@ +/** + * 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.storage; + +import com.google.common.base.Optional; +import net.spy.memcached.MemcachedClient; + +public class PendingDevicesManager { + + private static final String MEMCACHE_PREFIX = "pending_devices"; + + private final PendingDeviceRegistrations pendingDevices; + private final MemcachedClient memcachedClient; + + public PendingDevicesManager(PendingDeviceRegistrations pendingDevices, + MemcachedClient memcachedClient) + { + this.pendingDevices = pendingDevices; + this.memcachedClient = memcachedClient; + } + + public void store(String number, String code) { + if (memcachedClient != null) { + memcachedClient.set(MEMCACHE_PREFIX + number, 0, code); + } + + pendingDevices.insert(number, code); + } + + public void remove(String number) { + if (memcachedClient != null) + memcachedClient.delete(MEMCACHE_PREFIX + number); + pendingDevices.remove(number); + } + + public Optional getCodeForNumber(String number) { + String code = null; + + if (memcachedClient != null) { + code = (String)memcachedClient.get(MEMCACHE_PREFIX + number); + } + + if (code == null) { + code = pendingDevices.getCodeForNumber(number); + + if (code != null && memcachedClient != null) { + memcachedClient.set(MEMCACHE_PREFIX + number, 0, code); + } + } + + if (code != null) return Optional.of(code); + else return Optional.absent(); + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessageManager.java b/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessageManager.java new file mode 100644 index 000000000..d9d563bac --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessageManager.java @@ -0,0 +1,30 @@ +/** + * 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.storage; + +import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage; + +public class StoredMessageManager { + StoredMessages storedMessages; + public StoredMessageManager(StoredMessages storedMessages) { + this.storedMessages = storedMessages; + } + + public void storeMessage(Account account, EncryptedOutgoingMessage outgoingMessage) { + storedMessages.insert(account.getId(), outgoingMessage); + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessages.java b/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessages.java new file mode 100644 index 000000000..361771b63 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/StoredMessages.java @@ -0,0 +1,33 @@ +/** + * 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.storage; + +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.whispersystems.textsecuregcm.entities.EncryptedOutgoingMessage; + +import java.util.List; + +public interface StoredMessages { + + @SqlUpdate("INSERT INTO stored_messages (destination_id, encrypted_message) VALUES :destination_id, :encrypted_message") + void insert(@Bind("destination_id") long destinationAccountId, @Bind("encrypted_message") EncryptedOutgoingMessage encryptedOutgoingMessage); + + @SqlQuery("SELECT encrypted_message FROM stored_messages WHERE destination_id = :account_id") + List getMessagesForAccountId(@Bind("account_id") long accountId); +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/util/NumberData.java b/src/main/java/org/whispersystems/textsecuregcm/util/NumberData.java new file mode 100644 index 000000000..8fd446e61 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/util/NumberData.java @@ -0,0 +1,25 @@ +package org.whispersystems.textsecuregcm.util; + +public class NumberData { + private String number; + private boolean active; + private boolean supportsSms; + + public NumberData(String number, boolean active, boolean supportsSms) { + this.number = number; + this.active = active; + this.supportsSms = supportsSms; + } + + public boolean isActive() { + return active; + } + + public boolean isSupportsSms() { + return supportsSms; + } + + public String getNumber() { + return number; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java b/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java index e77b38770..41d56cad9 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java +++ b/src/main/java/org/whispersystems/textsecuregcm/util/VerificationCode.java @@ -16,12 +16,24 @@ */ package org.whispersystems.textsecuregcm.util; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.google.common.annotations.VisibleForTesting; + public class VerificationCode { + @JsonProperty private String verificationCode; + @JsonIgnore private String verificationCodeDisplay; + @JsonIgnore private String verificationCodeSpeech; + @VisibleForTesting VerificationCode() {} + public VerificationCode(int verificationCode) { this.verificationCode = verificationCode + ""; this.verificationCodeDisplay = this.verificationCode.substring(0, 3) + "-" + @@ -54,4 +66,7 @@ public class VerificationCode { return delimited; } + @VisibleForTesting public boolean equals(Object o) { + return o instanceof VerificationCode && verificationCode.equals(((VerificationCode) o).verificationCode); + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/workers/DirectoryUpdater.java b/src/main/java/org/whispersystems/textsecuregcm/workers/DirectoryUpdater.java index b489b2a59..916d70104 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/workers/DirectoryUpdater.java +++ b/src/main/java/org/whispersystems/textsecuregcm/workers/DirectoryUpdater.java @@ -27,6 +27,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.DirectoryManager; import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle; import org.whispersystems.textsecuregcm.util.Base64; +import org.whispersystems.textsecuregcm.util.NumberData; import org.whispersystems.textsecuregcm.util.Util; import java.util.Iterator; @@ -53,22 +54,22 @@ public class DirectoryUpdater { BatchOperationHandle batchOperation = directory.startBatchOperation(); try { - Iterator accounts = accountsManager.getAll(); + Iterator numbers = accountsManager.getAllNumbers(); - if (accounts == null) + if (numbers == null) return; - while (accounts.hasNext()) { - Account account = accounts.next(); - if (account.getApnRegistrationId() != null || account.getGcmRegistrationId() != null) { - byte[] token = Util.getContactToken(account.getNumber()); - ClientContact clientContact = new ClientContact(token, null, account.getSupportsSms()); + while (numbers.hasNext()) { + NumberData number = numbers.next(); + if (number.isActive()) { + byte[] token = Util.getContactToken(number.getNumber()); + ClientContact clientContact = new ClientContact(token, null, number.isSupportsSms()); directory.add(batchOperation, clientContact); logger.debug("Adding local token: " + Base64.encodeBytesWithoutPadding(token)); } else { - directory.remove(batchOperation, account.getNumber()); + directory.remove(batchOperation, number.getNumber()); } } } finally { diff --git a/src/main/resources/migrations.xml b/src/main/resources/migrations.xml index 8f4ddf51a..cf9e162aa 100644 --- a/src/main/resources/migrations.xml +++ b/src/main/resources/migrations.xml @@ -75,4 +75,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java b/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java index fee459848..38ce3dab3 100644 --- a/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java +++ b/src/test/org/whispersystems/textsecuregcm/tests/controllers/AccountControllerTest.java @@ -1,9 +1,15 @@ package org.whispersystems.textsecuregcm.tests.controllers; +import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.Optional; import com.sun.jersey.api.client.ClientResponse; import com.yammer.dropwizard.testing.ResourceTest; +import org.hibernate.validator.constraints.NotEmpty; 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.AccountController; import org.whispersystems.textsecuregcm.entities.AccountAttributes; import org.whispersystems.textsecuregcm.limits.RateLimiter; @@ -12,23 +18,54 @@ import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.PendingAccountsManager; +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.junit.Assert.assertEquals; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.*; public class AccountControllerTest extends ResourceTest { + /** The AccountAttributes used in protocol v1 (no fetchesMessages) */ + static class V1AccountAttributes { + @JsonProperty + @NotEmpty + private String signalingKey; + + @JsonProperty + private boolean supportsSms; + + public V1AccountAttributes(String signalingKey, boolean supportsSms) { + this.signalingKey = signalingKey; + this.supportsSms = supportsSms; + } + } + + @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); + } + + @Override + protected VerificationCode generateVerificationCode() { + return new VerificationCode(5678901); + } + } private static final String SENDER = "+14152222222"; - 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 SmsSender smsSender = mock(SmsSender.class ); + 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 ); + private SmsSender smsSender = mock(SmsSender.class ); @Override protected void setUpResources() throws Exception { @@ -40,10 +77,17 @@ public class AccountControllerTest extends ResourceTest { when(pendingAccountsManager.getCodeForNumber(SENDER)).thenReturn(Optional.of("1234")); - addResource(new AccountController(pendingAccountsManager, - accountsManager, - rateLimiters, - smsSender)); + 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 DumbVerificationAccountController(pendingAccountsManager, pendingDevicesManager, accountsManager, rateLimiters, smsSender)); } @Test @@ -62,13 +106,17 @@ public class AccountControllerTest extends ResourceTest { ClientResponse response = client().resource(String.format("/v1/accounts/code/%s", "1234")) .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) - .entity(new AccountAttributes("keykeykeykey", false)) + .entity(new V1AccountAttributes("keykeykeykey", false)) .type(MediaType.APPLICATION_JSON_TYPE) .put(ClientResponse.class); assertThat(response.getStatus()).isEqualTo(204); - verify(accountsManager).create(isA(Account.class)); + verify(accountsManager).createResetNumber(isA(Account.class)); + + ArgumentCaptor number = ArgumentCaptor.forClass(String.class); + verify(pendingAccountsManager).remove(number.capture()); + assertThat(number.getValue()).isEqualTo(SENDER); } @Test @@ -76,7 +124,7 @@ public class AccountControllerTest extends ResourceTest { ClientResponse response = client().resource(String.format("/v1/accounts/code/%s", "1111")) .header("Authorization", AuthHelper.getAuthHeader(SENDER, "bar")) - .entity(new AccountAttributes("keykeykeykey", false)) + .entity(new V1AccountAttributes("keykeykeykey", false)) .type(MediaType.APPLICATION_JSON_TYPE) .put(ClientResponse.class); @@ -85,4 +133,28 @@ 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/KeyControllerTest.java b/src/test/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java index 3b8bd886c..a41e7fa16 100644 --- a/src/test/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java +++ b/src/test/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java @@ -1,15 +1,24 @@ package org.whispersystems.textsecuregcm.tests.controllers; import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.GenericType; import com.yammer.dropwizard.testing.ResourceTest; import org.junit.Test; import org.whispersystems.textsecuregcm.controllers.KeysController; import org.whispersystems.textsecuregcm.entities.PreKey; +import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; 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.Keys; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; +import javax.jws.WebResult; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Mockito.*; @@ -18,26 +27,41 @@ public class KeyControllerTest extends ResourceTest { private final String EXISTS_NUMBER = "+14152222222"; private final String NOT_EXISTS_NUMBER = "+14152222220"; - private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, 1234, "test1", "test2", false); - private final Keys keys = mock(Keys.class); + private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, AuthHelper.DEFAULT_DEVICE_ID, 1234, "test1", "test2", false); + private final PreKey SAMPLE_KEY2 = new PreKey(2, EXISTS_NUMBER, 2, 5667, "test3", "test4", false); + private final Keys keys = mock(Keys.class); + + Account[] fakeAccount; @Override protected void setUpResources() { addProvider(AuthHelper.getAuthenticator()); - RateLimiters rateLimiters = mock(RateLimiters.class); - RateLimiter rateLimiter = mock(RateLimiter.class ); + RateLimiters rateLimiters = mock(RateLimiters.class); + RateLimiter rateLimiter = mock(RateLimiter.class ); + AccountsManager accounts = mock(AccountsManager.class); + + fakeAccount = new Account[2]; + fakeAccount[0] = mock(Account.class); + fakeAccount[1] = mock(Account.class); when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter); - when(keys.get(EXISTS_NUMBER)).thenReturn(SAMPLE_KEY); - when(keys.get(NOT_EXISTS_NUMBER)).thenReturn(null); + when(keys.get(eq(EXISTS_NUMBER), anyList())).thenReturn(new UnstructuredPreKeyList(Arrays.asList(SAMPLE_KEY, SAMPLE_KEY2))); + when(keys.get(eq(NOT_EXISTS_NUMBER), anyList())).thenReturn(null); - addResource(new KeysController(rateLimiters, keys, null)); + when(fakeAccount[0].getDeviceId()).thenReturn(AuthHelper.DEFAULT_DEVICE_ID); + when(fakeAccount[1].getDeviceId()).thenReturn((long) 2); + + when(accounts.getAllByNumber(EXISTS_NUMBER)).thenReturn(Arrays.asList(fakeAccount[0], fakeAccount[1])); + when(accounts.getAllByNumber(NOT_EXISTS_NUMBER)).thenReturn(new LinkedList()); + + addResource(new KeysController.V1(rateLimiters, keys, accounts, null)); + addResource(new KeysController.V2(rateLimiters, keys, accounts, null)); } @Test - public void validRequestTest() throws Exception { + public void validRequestsTest() throws Exception { PreKey result = client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .get(PreKey.class); @@ -49,7 +73,32 @@ public class KeyControllerTest extends ResourceTest { assertThat(result.getId() == 0); assertThat(result.getNumber() == null); - verify(keys).get(EXISTS_NUMBER); + verify(keys).get(eq(EXISTS_NUMBER), eq(Arrays.asList(fakeAccount))); + verifyNoMoreInteractions(keys); + + List results = client().resource(String.format("/v2/keys/%s", EXISTS_NUMBER)) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(new GenericType>(){}); + + assertThat(results.size()).isEqualTo(2); + result = results.get(0); + assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId()); + assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey()); + assertThat(result.getIdentityKey()).isEqualTo(SAMPLE_KEY.getIdentityKey()); + + assertThat(result.getId() == 0); + assertThat(result.getNumber() == null); + + result = results.get(1); + assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId()); + assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey()); + assertThat(result.getIdentityKey()).isEqualTo(SAMPLE_KEY2.getIdentityKey()); + + assertThat(result.getId() == 1); + assertThat(result.getNumber() == null); + + verify(keys, times(2)).get(eq(EXISTS_NUMBER), eq(Arrays.asList(fakeAccount[0], fakeAccount[1]))); + verifyNoMoreInteractions(keys); } @Test @@ -60,7 +109,7 @@ public class KeyControllerTest extends ResourceTest { assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(404); - verify(keys).get(NOT_EXISTS_NUMBER); + verify(keys).get(NOT_EXISTS_NUMBER, new LinkedList()); } @Test diff --git a/src/test/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java b/src/test/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java index 5295afc77..a912d5c6a 100644 --- a/src/test/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java +++ b/src/test/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java @@ -14,7 +14,7 @@ public class PreKeyTest { @Test public void serializeToJSON() throws Exception { - PreKey preKey = new PreKey(1, "+14152222222", 1234, "test", "identityTest", false); + PreKey preKey = new PreKey(1, "+14152222222", 0, 1234, "test", "identityTest", false); assertThat("Basic Contact Serialization works", asJson(preKey), diff --git a/src/test/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java b/src/test/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java index 636a01f73..deb9182dc 100644 --- a/src/test/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java +++ b/src/test/org/whispersystems/textsecuregcm/tests/util/AuthHelper.java @@ -15,6 +15,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class AuthHelper { + public static final long DEFAULT_DEVICE_ID = 1; public static final String VALID_NUMBER = "+14150000000"; public static final String VALID_PASSWORD = "foo"; @@ -29,7 +30,7 @@ public class AuthHelper { when(credentials.verify("foo")).thenReturn(true); when(account.getAuthenticationCredentials()).thenReturn(credentials); - when(accounts.get(VALID_NUMBER)).thenReturn(Optional.of(account)); + when(accounts.get(VALID_NUMBER, DEFAULT_DEVICE_ID)).thenReturn(Optional.of(account)); return new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(new FederationConfiguration()), FederatedPeer.class, @@ -41,4 +42,7 @@ public class AuthHelper { return "Basic " + Base64.encodeBytes((number + ":" + password).getBytes()); } + public static String getV2AuthHeader(String number, long deviceId, String password) { + return "Basic " + Base64.encodeBytes((number + "." + deviceId + ":" + password).getBytes()); + } }