0
0
mirror of https://github.com/signalapp/Signal-Server.git synced 2024-09-19 19:42:18 +02:00

New API to support multiple accounts per # (FREEBIE)

This commit is contained in:
Matt Corallo 2014-01-07 13:35:58 -10:00
parent 4cd1082a4a
commit ef1160eda8
35 changed files with 1591 additions and 388 deletions

View File

@ -56,7 +56,7 @@
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.4.1</version>
<version>2.5.0</version>
</dependency>
<dependency>

View File

@ -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;
}

View File

@ -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<WhisperServerConfiguration> {
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> nexmoSmsSender = initializeNexmoSmsSender(config.getNexmoConfiguration());
@ -109,6 +117,7 @@ public class WhisperServerService extends Service<WhisperServerConfiguration> {
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<WhisperServerConfiguration> {
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,

View File

@ -51,7 +51,13 @@ public class AccountAuthenticator implements Authenticator<BasicCredentials, Acc
public Optional<Account> authenticate(BasicCredentials basicCredentials)
throws AuthenticationException
{
Optional<Account> account = accountsManager.get(basicCredentials.getUsername());
AuthorizationHeader authorizationHeader;
try {
authorizationHeader = AuthorizationHeader.fromUserAndPassword(basicCredentials.getUsername(), basicCredentials.getPassword());
} catch (InvalidAuthorizationHeaderException iahe) {
return Optional.absent();
}
Optional<Account> account = accountsManager.get(authorizationHeader.getNumber(), authorizationHeader.getDeviceId());
if (!account.isPresent()) {
return Optional.absent();

View File

@ -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() {

View File

@ -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<String> 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();
}
}

View File

@ -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<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
List<NumberData> numberList = accounts.getAllNumbers(offset, ACCOUNT_CHUNK_SIZE);
List<ClientContact> 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);
}

View File

@ -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<PreKey> 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<PreKey> get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") String relay)
throws RateLimitExceededException
{
return super.getKeys(account, number, relay);
}
}
}

View File

@ -138,12 +138,13 @@ public class MessageController extends HttpServlet {
try {
for (Pair<IncomingMessage, OutgoingMessageSignal> 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> account = accountAuthenticator.authenticate(credentials);

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<PreKey> keys;
public UnstructuredPreKeyList(List<PreKey> preKeys) {
this.keys = preKeys;
}
public List<PreKey> getKeys() {
return keys;
}
@VisibleForTesting public boolean equals(Object o) {
if (!(o instanceof UnstructuredPreKeyList) ||
((UnstructuredPreKeyList) o).keys.size() != keys.size())
return false;
Iterator<PreKey> otherKeys = ((UnstructuredPreKeyList) o).keys.iterator();
for (PreKey key : keys) {
if (!otherKeys.next().equals(key))
return false;
}
return true;
}
}

View File

@ -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) {

View File

@ -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> account = accounts.get(destination);
Optional<Account> 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);
}
}

View File

@ -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;
}
}

View File

@ -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<NumberData> getAllNumbers(@Bind("offset") int offset, @Bind("limit") int length);
@Mapper(NumberDataMapper.class)
@SqlQuery(NUMBER_DATA_QUERY + " GROUP BY " + NUMBER)
public abstract Iterator<NumberData> 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<Account> 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<Account> getAll(@Bind("offset") int offset, @Bind("limit") int length);
@Mapper(AccountMapper.class)
@SqlQuery("SELECT * FROM accounts")
abstract Iterator<Account> 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<Account> {
@ -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<NumberData> {
@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);
}
};
}

View File

@ -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<Account> getAll(int offset, int length) {
return accounts.getAll(offset, length);
public List<NumberData> getAllNumbers(int offset, int length) {
return accounts.getAllNumbers(offset, length);
}
public Iterator<Account> getAll() {
return accounts.getAll();
public Iterator<NumberData> 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<Account> get(String number) {
public Optional<Account> 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<Account> 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;
}
}

View File

@ -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<PreKey> 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<PreKey> keys) {
public void store(String number, long deviceId, PreKey lastResortKey, List<PreKey> 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<Account> accounts) {
List<PreKey> 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);

View File

@ -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);
}

View File

@ -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<String> getCodeForNumber(String number) {
String code = null;

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<EncryptedOutgoingMessage> getMessagesForAccountId(@Bind("account_id") long accountId);
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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<Account> accounts = accountsManager.getAll();
Iterator<NumberData> 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 {

View File

@ -75,4 +75,51 @@
<column name="number"/>
</createIndex>
</changeSet>
<changeSet id="2" author="matt">
<addColumn tableName="accounts">
<column name="device_id" type="bigint">
<constraints nullable="false" />
</column>
<column name="fetches_messages" type="smallint" defaultValue="0"/>
</addColumn>
<dropUniqueConstraint tableName="accounts" constraintName="accounts_number_key" />
<addUniqueConstraint constraintName="account_number_device_unique" tableName="accounts" columnNames="number, device_id" />
<addColumn tableName="keys">
<column name="device_id" type="bigint" >
<constraints nullable="false" />
</column>
</addColumn>
<createTable tableName="pending_devices">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="number" type="varchar(255)">
<constraints unique="true" nullable="false"/>
</column>
<column name="verification_code" type="varchar(255)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="stored_messages">
<column name="id" type="bigint" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="destination_id" type="bigint">
<constraints nullable="false" foreignKeyName="destination_fk" references="accounts(id)"/>
</column>
<column name="encrypted_message" type="text">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
</databaseChangeLog>

View File

@ -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<String> 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<Account> newAccount = ArgumentCaptor.forClass(Account.class);
verify(accountsManager).createAccountOnExistingNumber(newAccount.capture());
assertThat(deviceId).isEqualTo(newAccount.getValue().getDeviceId());
ArgumentCaptor<String> number = ArgumentCaptor.forClass(String.class);
verify(pendingDevicesManager).remove(number.capture());
assertThat(number.getValue()).isEqualTo(AuthHelper.VALID_NUMBER);
}
}

View File

@ -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<Account>());
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<PreKey> results = client().resource(String.format("/v2/keys/%s", EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(new GenericType<List<PreKey>>(){});
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<Account>());
}
@Test

View File

@ -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),

View File

@ -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());
}
}