Introduce V2 API for PreKey updates and requests.

1) A /v2/keys controller.

2) Separate wire protocol PreKey POJOs from database PreKey
   objects.

3) Separate wire protocol PreKey submission and response POJOs.

4) Introduce a new update/response JSON format for /v2/keys.
This commit is contained in:
Moxie Marlinspike 2014-07-01 15:06:48 -07:00
parent d9de015eab
commit 06f80c320d
26 changed files with 1116 additions and 346 deletions

View File

@ -33,7 +33,8 @@ import org.whispersystems.textsecuregcm.controllers.AttachmentController;
import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.controllers.DeviceController;
import org.whispersystems.textsecuregcm.controllers.DirectoryController; import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.controllers.FederationController; import org.whispersystems.textsecuregcm.controllers.FederationController;
import org.whispersystems.textsecuregcm.controllers.KeysController; import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
import org.whispersystems.textsecuregcm.controllers.MessageController; import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager; import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.FederatedPeer; import org.whispersystems.textsecuregcm.federation.FederatedPeer;
@ -147,7 +148,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
accountsManager); accountsManager);
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner); AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, federatedClientManager); KeysControllerV1 keysControllerV1 = new KeysControllerV1(rateLimiters, keys, accountsManager, federatedClientManager);
KeysControllerV2 keysControllerV2 = new KeysControllerV2(rateLimiters, keys, accountsManager, federatedClientManager);
MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager); MessageController messageController = new MessageController(rateLimiters, pushSender, accountsManager, federatedClientManager);
environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()), environment.jersey().register(new MultiBasicAuthProvider<>(new FederatedPeerAuthenticator(config.getFederationConfiguration()),
@ -158,9 +160,10 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender)); environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters)); environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
environment.jersey().register(new DirectoryController(rateLimiters, directory)); environment.jersey().register(new DirectoryController(rateLimiters, directory));
environment.jersey().register(new FederationController(accountsManager, attachmentController, keysController, messageController)); environment.jersey().register(new FederationController(accountsManager, attachmentController, keysControllerV1, keysControllerV2, messageController));
environment.jersey().register(attachmentController); environment.jersey().register(attachmentController);
environment.jersey().register(keysController); environment.jersey().register(keysControllerV1);
environment.jersey().register(keysControllerV2);
environment.jersey().register(messageController); environment.jersey().register(messageController);
if (config.getWebsocketConfiguration().isEnabled()) { if (config.getWebsocketConfiguration().isEnabled()) {

View File

@ -25,8 +25,9 @@ import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts; import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList; import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.PreKey; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; import org.whispersystems.textsecuregcm.entities.PreKeyV1;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.federation.FederatedPeer; import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount; import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
@ -46,7 +47,7 @@ import java.util.List;
import io.dropwizard.auth.Auth; import io.dropwizard.auth.Auth;
@Path("/v1/federation") @Path("/")
public class FederationController { public class FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationController.class); private final Logger logger = LoggerFactory.getLogger(FederationController.class);
@ -55,23 +56,26 @@ public class FederationController {
private final AccountsManager accounts; private final AccountsManager accounts;
private final AttachmentController attachmentController; private final AttachmentController attachmentController;
private final KeysController keysController; private final KeysControllerV1 keysControllerV1;
private final KeysControllerV2 keysControllerV2;
private final MessageController messageController; private final MessageController messageController;
public FederationController(AccountsManager accounts, public FederationController(AccountsManager accounts,
AttachmentController attachmentController, AttachmentController attachmentController,
KeysController keysController, KeysControllerV1 keysControllerV1,
MessageController messageController) KeysControllerV2 keysControllerV2,
MessageController messageController)
{ {
this.accounts = accounts; this.accounts = accounts;
this.attachmentController = attachmentController; this.attachmentController = attachmentController;
this.keysController = keysController; this.keysControllerV1 = keysControllerV1;
this.keysControllerV2 = keysControllerV2;
this.messageController = messageController; this.messageController = messageController;
} }
@Timed @Timed
@GET @GET
@Path("/attachment/{attachmentId}") @Path("/v1/federation/attachment/{attachmentId}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer, public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
@PathParam("attachmentId") long attachmentId) @PathParam("attachmentId") long attachmentId)
@ -83,14 +87,15 @@ public class FederationController {
@Timed @Timed
@GET @GET
@Path("/key/{number}") @Path("/v1/federation/key/{number}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public PreKey getKey(@Auth FederatedPeer peer, public Optional<PreKeyV1> getKey(@Auth FederatedPeer peer,
@PathParam("number") String number) @PathParam("number") String number)
throws IOException throws IOException
{ {
try { try {
return keysController.get(new NonLimitedAccount("Unknown", -1, peer.getName()), number, Optional.<String>absent()); return keysControllerV1.get(new NonLimitedAccount("Unknown", -1, peer.getName()),
number, Optional.<String>absent());
} catch (RateLimitExceededException e) { } catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e); logger.warn("Rate limiting on federated channel", e);
throw new IOException(e); throw new IOException(e);
@ -99,16 +104,34 @@ public class FederationController {
@Timed @Timed
@GET @GET
@Path("/key/{number}/{device}") @Path("/v1/federation/key/{number}/{device}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public UnstructuredPreKeyList getKeys(@Auth FederatedPeer peer, public Optional<PreKeyResponseV1> getKeysV1(@Auth FederatedPeer peer,
@PathParam("number") String number, @PathParam("number") String number,
@PathParam("device") String device) @PathParam("device") String device)
throws IOException throws IOException
{ {
try { try {
return keysController.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()), return keysControllerV1.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()),
number, device, Optional.<String>absent()); number, device, Optional.<String>absent());
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@GET
@Path("/v2/federation/key/{number}/{device}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV2> getKeysV2(@Auth FederatedPeer peer,
@PathParam("number") String number,
@PathParam("device") String device)
throws IOException
{
try {
return keysControllerV2.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()),
number, device, Optional.<String>absent());
} catch (RateLimitExceededException e) { } catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e); logger.warn("Rate limiting on federated channel", e);
throw new IOException(e); throw new IOException(e);
@ -117,7 +140,7 @@ public class FederationController {
@Timed @Timed
@PUT @PUT
@Path("/messages/{source}/{sourceDeviceId}/{destination}") @Path("/v1/federation/messages/{source}/{sourceDeviceId}/{destination}")
public void sendMessages(@Auth FederatedPeer peer, public void sendMessages(@Auth FederatedPeer peer,
@PathParam("source") String source, @PathParam("source") String source,
@PathParam("sourceDeviceId") long sourceDeviceId, @PathParam("sourceDeviceId") long sourceDeviceId,
@ -136,7 +159,7 @@ public class FederationController {
@Timed @Timed
@GET @GET
@Path("/user_count") @Path("/v1/federation/user_count")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public AccountCount getUserCount(@Auth FederatedPeer peer) { public AccountCount getUserCount(@Auth FederatedPeer peer) {
return new AccountCount((int)accounts.getCount()); return new AccountCount((int)accounts.getCount());
@ -144,7 +167,7 @@ public class FederationController {
@Timed @Timed
@GET @GET
@Path("/user_tokens/{offset}") @Path("/v1/federation/user_tokens/{offset}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public ClientContacts getUserTokens(@Auth FederatedPeer peer, public ClientContacts getUserTokens(@Auth FederatedPeer peer,
@PathParam("offset") int offset) @PathParam("offset") int offset)

View File

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2013 Open WhisperSystems * Copyright (C) 2014 Open Whisper Systems
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -18,45 +18,30 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed; import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import org.slf4j.Logger; import org.whispersystems.textsecuregcm.entities.PreKeyCount;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyList;
import org.whispersystems.textsecuregcm.entities.PreKeyStatus;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager; import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys; import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException; import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import io.dropwizard.auth.Auth; import io.dropwizard.auth.Auth;
@Path("/v1/keys")
public class KeysController { public class KeysController {
private final Logger logger = LoggerFactory.getLogger(KeysController.class); protected final RateLimiters rateLimiters;
protected final Keys keys;
private final RateLimiters rateLimiters; protected final AccountsManager accounts;
private final Keys keys; protected final FederatedClientManager federatedClientManager;
private final AccountsManager accounts;
private final FederatedClientManager federatedClientManager;
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts, public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
FederatedClientManager federatedClientManager) FederatedClientManager federatedClientManager)
@ -67,119 +52,65 @@ public class KeysController {
this.federatedClientManager = federatedClientManager; this.federatedClientManager = federatedClientManager;
} }
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyList preKeys) {
Device device = account.getAuthenticatedDevice().get();
String identityKey = preKeys.getLastResortKey().getIdentityKey();
if (!identityKey.equals(account.getIdentityKey())) {
account.setIdentityKey(identityKey);
accounts.update(account);
}
keys.store(account.getNumber(), device.getId(), preKeys.getKeys(), preKeys.getLastResortKey());
}
@Timed @Timed
@GET @GET
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
public PreKeyStatus getStatus(@Auth Account account) { public PreKeyCount getStatus(@Auth Account account) {
int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId()); int count = keys.getCount(account.getNumber(), account.getAuthenticatedDevice().get().getId());
if (count > 0) { if (count > 0) {
count = count - 1; count = count - 1;
} }
return new PreKeyStatus(count); return new PreKeyCount(count);
} }
@Timed protected TargetKeys getLocalKeys(String number, String deviceIdSelector)
@GET throws NoSuchUserException
@Path("/{number}/{device_id}")
@Produces(MediaType.APPLICATION_JSON)
public UnstructuredPreKeyList getDeviceKey(@Auth Account account,
@PathParam("number") String number,
@PathParam("device_id") String deviceId,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{ {
try {
if (account.isRateLimited()) {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
}
Optional<UnstructuredPreKeyList> results;
if (!relay.isPresent()) results = getLocalKeys(number, deviceId);
else results = federatedClientManager.getClient(relay.get()).getKeys(number, deviceId);
if (results.isPresent()) return results.get();
else throw new WebApplicationException(Response.status(404).build());
} catch (NoSuchPeerException e) {
throw new WebApplicationException(Response.status(404).build());
}
}
@Timed
@GET
@Path("/{number}")
@Produces(MediaType.APPLICATION_JSON)
public PreKey get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
UnstructuredPreKeyList results = getDeviceKey(account, number, String.valueOf(Device.MASTER_ID), relay);
return results.getKeys().get(0);
}
private Optional<UnstructuredPreKeyList> getLocalKeys(String number, String deviceIdSelector) {
Optional<Account> destination = accounts.get(number); Optional<Account> destination = accounts.get(number);
if (!destination.isPresent() || !destination.get().isActive()) { if (!destination.isPresent() || !destination.get().isActive()) {
return Optional.absent(); throw new NoSuchUserException("Target account is inactive");
} }
try { try {
if (deviceIdSelector.equals("*")) { if (deviceIdSelector.equals("*")) {
Optional<UnstructuredPreKeyList> preKeys = keys.get(number); Optional<List<KeyRecord>> preKeys = keys.get(number);
return getActiveKeys(destination.get(), preKeys); return new TargetKeys(destination.get(), preKeys);
} }
long deviceId = Long.parseLong(deviceIdSelector); long deviceId = Long.parseLong(deviceIdSelector);
Optional<Device> targetDevice = destination.get().getDevice(deviceId); Optional<Device> targetDevice = destination.get().getDevice(deviceId);
if (!targetDevice.isPresent() || !targetDevice.get().isActive()) { if (!targetDevice.isPresent() || !targetDevice.get().isActive()) {
return Optional.absent(); throw new NoSuchUserException("Target device is inactive.");
} }
Optional<UnstructuredPreKeyList> preKeys = keys.get(number, deviceId); Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
return getActiveKeys(destination.get(), preKeys); return new TargetKeys(destination.get(), preKeys);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
throw new WebApplicationException(Response.status(422).build()); throw new WebApplicationException(Response.status(422).build());
} }
} }
private Optional<UnstructuredPreKeyList> getActiveKeys(Account destination,
Optional<UnstructuredPreKeyList> preKeys)
{
if (!preKeys.isPresent()) return Optional.absent();
List<PreKey> filteredKeys = new LinkedList<>(); public static class TargetKeys {
private final Account destination;
private final Optional<List<KeyRecord>> keys;
for (PreKey preKey : preKeys.get().getKeys()) { public TargetKeys(Account destination, Optional<List<KeyRecord>> keys) {
Optional<Device> device = destination.getDevice(preKey.getDeviceId()); this.destination = destination;
this.keys = keys;
if (device.isPresent() && device.get().isActive()) {
preKey.setRegistrationId(device.get().getRegistrationId());
preKey.setIdentityKey(destination.getIdentityKey());
filteredKeys.add(preKey);
}
} }
if (filteredKeys.isEmpty()) return Optional.absent(); public Optional<List<KeyRecord>> getKeys() {
else return Optional.of(new UnstructuredPreKeyList(filteredKeys)); return keys;
}
public Account getDestination() {
return destination;
}
} }
} }

View File

@ -0,0 +1,136 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyStateV1;
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
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.Device;
import org.whispersystems.textsecuregcm.storage.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
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.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v1/keys")
public class KeysControllerV1 extends KeysController {
private final Logger logger = LoggerFactory.getLogger(KeysControllerV1.class);
public KeysControllerV1(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
FederatedClientManager federatedClientManager)
{
super(rateLimiters, keys, accounts, federatedClientManager);
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyStateV1 preKeys) {
Device device = account.getAuthenticatedDevice().get();
String identityKey = preKeys.getLastResortKey().getIdentityKey();
if (!identityKey.equals(account.getIdentityKey())) {
account.setIdentityKey(identityKey);
accounts.update(account);
}
keys.store(account.getNumber(), device.getId(), preKeys.getKeys(), preKeys.getLastResortKey());
}
@Timed
@GET
@Path("/{number}/{device_id}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV1> getDeviceKey(@Auth Account account,
@PathParam("number") String number,
@PathParam("device_id") String deviceId,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
try {
if (account.isRateLimited()) {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
}
if (relay.isPresent()) {
return federatedClientManager.getClient(relay.get()).getKeysV1(number, deviceId);
}
TargetKeys targetKeys = getLocalKeys(number, deviceId);
if (!targetKeys.getKeys().isPresent()) {
return Optional.absent();
}
List<PreKeyV1> preKeys = new LinkedList<>();
Account destination = targetKeys.getDestination();
for (KeyRecord record : targetKeys.getKeys().get()) {
Optional<Device> device = destination.getDevice(record.getDeviceId());
if (device.isPresent() && device.get().isActive()) {
preKeys.add(new PreKeyV1(record.getDeviceId(), record.getKeyId(),
record.getPublicKey(), destination.getIdentityKey(),
device.get().getRegistrationId()));
}
}
if (preKeys.isEmpty()) return Optional.absent();
else return Optional.of(new PreKeyResponseV1(preKeys));
} catch (NoSuchPeerException | NoSuchUserException e) {
throw new WebApplicationException(Response.status(404).build());
}
}
@Timed
@GET
@Path("/{number}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyV1> get(@Auth Account account,
@PathParam("number") String number,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
Optional<PreKeyResponseV1> results = getDeviceKey(account, number, String.valueOf(Device.MASTER_ID), relay);
if (results.isPresent()) return Optional.of(results.get().getKeys().get(0));
else return Optional.absent();
}
}

View File

@ -0,0 +1,147 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.entities.DeviceKey;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItemV2;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.entities.PreKeyStateV2;
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
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.Device;
import org.whispersystems.textsecuregcm.storage.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
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.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v2/keys")
public class KeysControllerV2 extends KeysController {
public KeysControllerV2(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
FederatedClientManager federatedClientManager)
{
super(rateLimiters, keys, accounts, federatedClientManager);
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void setKeys(@Auth Account account, @Valid PreKeyStateV2 preKeys) {
Device device = account.getAuthenticatedDevice().get();
boolean updateAccount = false;
if (!preKeys.getDeviceKey().equals(device.getDeviceKey())) {
device.setDeviceKey(preKeys.getDeviceKey());
updateAccount = true;
}
if (!preKeys.getIdentityKey().equals(account.getIdentityKey())) {
account.setIdentityKey(preKeys.getIdentityKey());
updateAccount = true;
}
if (updateAccount) {
accounts.update(account);
}
keys.store(account.getNumber(), device.getId(), preKeys.getPreKeys(), preKeys.getLastResortKey());
}
@Timed
@PUT
@Path("/device")
@Consumes(MediaType.APPLICATION_JSON)
public void setDeviceKey(@Auth Account account, @Valid DeviceKey deviceKey) {
Device device = account.getAuthenticatedDevice().get();
device.setDeviceKey(deviceKey);
accounts.update(account);
}
@Timed
@GET
@Path("/{number}/{device_id}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponseV2> getDeviceKey(@Auth Account account,
@PathParam("number") String number,
@PathParam("device_id") String deviceId,
@QueryParam("relay") Optional<String> relay)
throws RateLimitExceededException
{
try {
if (account.isRateLimited()) {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
}
if (relay.isPresent()) {
return federatedClientManager.getClient(relay.get()).getKeysV2(number, deviceId);
}
TargetKeys targetKeys = getLocalKeys(number, deviceId);
Account destination = targetKeys.getDestination();
List<PreKeyResponseItemV2> devices = new LinkedList<>();
for (Device device : destination.getDevices()) {
if (device.isActive() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
DeviceKey deviceKey = device.getDeviceKey();
PreKeyV2 preKey = null;
if (targetKeys.getKeys().isPresent()) {
for (KeyRecord keyRecord : targetKeys.getKeys().get()) {
if (keyRecord.getDeviceId() == device.getId()) {
preKey = new PreKeyV2(keyRecord.getKeyId(), keyRecord.getPublicKey());
}
}
}
if (deviceKey != null || preKey != null) {
devices.add(new PreKeyResponseItemV2(device.getId(), device.getRegistrationId(), deviceKey, preKey));
}
}
}
if (devices.isEmpty()) return Optional.absent();
else return Optional.of(new PreKeyResponseV2(destination.getIdentityKey(), devices));
} catch (NoSuchPeerException | NoSuchUserException e) {
throw new WebApplicationException(Response.status(404).build());
}
}
}

View File

@ -0,0 +1,46 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import java.io.Serializable;
public class DeviceKey extends PreKeyV2 implements Serializable {
@JsonProperty
@NotEmpty
private String signature;
public DeviceKey() {}
public DeviceKey(long keyId, String publicKey, String signature) {
super(keyId, publicKey);
this.signature = signature;
}
public String getSignature() {
return signature;
}
@Override
public boolean equals(Object object) {
if (object == null || !(object instanceof DeviceKey)) return false;
DeviceKey that = (DeviceKey) object;
if (signature == null) {
return super.equals(object) && that.signature == null;
} else {
return super.equals(object) && this.signature.equals(that.signature);
}
}
@Override
public int hashCode() {
if (signature == null) {
return super.hashCode();
} else {
return super.hashCode() ^ signature.hashCode();
}
}
}

View File

@ -0,0 +1,8 @@
package org.whispersystems.textsecuregcm.entities;
public interface PreKeyBase {
public long getKeyId();
public String getPublicKey();
}

View File

@ -3,16 +3,16 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
public class PreKeyStatus { public class PreKeyCount {
@JsonProperty @JsonProperty
private int count; private int count;
public PreKeyStatus(int count) { public PreKeyCount(int count) {
this.count = count; this.count = count;
} }
public PreKeyStatus() {} public PreKeyCount() {}
public int getCount() { public int getCount() {
return count; return count;

View File

@ -0,0 +1,64 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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;
public class PreKeyResponseItemV2 {
@JsonProperty
private long deviceId;
@JsonProperty
private int registrationId;
@JsonProperty
private DeviceKey deviceKey;
@JsonProperty
private PreKeyV2 preKey;
public PreKeyResponseItemV2() {}
public PreKeyResponseItemV2(long deviceId, int registrationId, DeviceKey deviceKey, PreKeyV2 preKey) {
this.deviceId = deviceId;
this.registrationId = registrationId;
this.deviceKey = deviceKey;
this.preKey = preKey;
}
@VisibleForTesting
public DeviceKey getDeviceKey() {
return deviceKey;
}
@VisibleForTesting
public PreKeyV2 getPreKey() {
return preKey;
}
@VisibleForTesting
public int getRegistrationId() {
return registrationId;
}
@VisibleForTesting
public long getDeviceId() {
return deviceId;
}
}

View File

@ -18,7 +18,6 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@ -26,36 +25,36 @@ import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
public class UnstructuredPreKeyList { public class PreKeyResponseV1 {
@JsonProperty @JsonProperty
@NotNull @NotNull
@Valid @Valid
private List<PreKey> keys; private List<PreKeyV1> keys;
@VisibleForTesting @VisibleForTesting
public UnstructuredPreKeyList() {} public PreKeyResponseV1() {}
public UnstructuredPreKeyList(PreKey preKey) { public PreKeyResponseV1(PreKeyV1 preKey) {
this.keys = new LinkedList<PreKey>(); this.keys = new LinkedList<>();
this.keys.add(preKey); this.keys.add(preKey);
} }
public UnstructuredPreKeyList(List<PreKey> preKeys) { public PreKeyResponseV1(List<PreKeyV1> preKeys) {
this.keys = preKeys; this.keys = preKeys;
} }
public List<PreKey> getKeys() { public List<PreKeyV1> getKeys() {
return keys; return keys;
} }
@VisibleForTesting @VisibleForTesting
public boolean equals(Object o) { public boolean equals(Object o) {
if (!(o instanceof UnstructuredPreKeyList) || if (!(o instanceof PreKeyResponseV1) ||
((UnstructuredPreKeyList) o).keys.size() != keys.size()) ((PreKeyResponseV1) o).keys.size() != keys.size())
return false; return false;
Iterator<PreKey> otherKeys = ((UnstructuredPreKeyList) o).keys.iterator(); Iterator<PreKeyV1> otherKeys = ((PreKeyResponseV1) o).keys.iterator();
for (PreKey key : keys) { for (PreKeyV1 key : keys) {
if (!otherKeys.next().equals(key)) if (!otherKeys.next().equals(key))
return false; return false;
} }
@ -64,7 +63,7 @@ public class UnstructuredPreKeyList {
public int hashCode() { public int hashCode() {
int ret = 0xFBA4C795 * keys.size(); int ret = 0xFBA4C795 * keys.size();
for (PreKey key : keys) for (PreKeyV1 key : keys)
ret ^= key.getPublicKey().hashCode(); ret ^= key.getPublicKey().hashCode();
return ret; return ret;
} }

View File

@ -0,0 +1,48 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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 java.util.List;
public class PreKeyResponseV2 {
@JsonProperty
private String identityKey;
@JsonProperty
private List<PreKeyResponseItemV2> devices;
public PreKeyResponseV2() {}
public PreKeyResponseV2(String identityKey, List<PreKeyResponseItemV2> devices) {
this.identityKey = identityKey;
this.devices = devices;
}
@VisibleForTesting
public String getIdentityKey() {
return identityKey;
}
@VisibleForTesting
public List<PreKeyResponseItemV2> getDevices() {
return devices;
}
}

View File

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2013 Open WhisperSystems * Copyright (C) 2014 Open Whisper Systems
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -18,39 +18,38 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.List; import java.util.List;
public class PreKeyList { public class PreKeyStateV1 {
@JsonProperty @JsonProperty
@NotNull @NotNull
@Valid @Valid
private PreKey lastResortKey; private PreKeyV1 lastResortKey;
@JsonProperty @JsonProperty
@NotNull @NotNull
@Valid @Valid
private List<PreKey> keys; private List<PreKeyV1> keys;
public List<PreKey> getKeys() { public List<PreKeyV1> getKeys() {
return keys; return keys;
} }
@VisibleForTesting @VisibleForTesting
public void setKeys(List<PreKey> keys) { public void setKeys(List<PreKeyV1> keys) {
this.keys = keys; this.keys = keys;
} }
public PreKey getLastResortKey() { public PreKeyV1 getLastResortKey() {
return lastResortKey; return lastResortKey;
} }
@VisibleForTesting @VisibleForTesting
public void setLastResortKey(PreKey lastResortKey) { public void setLastResortKey(PreKeyV1 lastResortKey) {
this.lastResortKey = lastResortKey; this.lastResortKey = lastResortKey;
} }
} }

View File

@ -0,0 +1,74 @@
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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.List;
public class PreKeyStateV2 {
@JsonProperty
@NotNull
@Valid
private List<PreKeyV2> preKeys;
@JsonProperty
@NotNull
@Valid
private DeviceKey deviceKey;
@JsonProperty
@NotNull
@Valid
private PreKeyV2 lastResortKey;
@JsonProperty
@NotEmpty
private String identityKey;
public PreKeyStateV2() {}
@VisibleForTesting
public PreKeyStateV2(String identityKey, DeviceKey deviceKey, List<PreKeyV2> keys, PreKeyV2 lastResortKey) {
this.identityKey = identityKey;
this.deviceKey = deviceKey;
this.preKeys = keys;
this.lastResortKey = lastResortKey;
}
public List<PreKeyV2> getPreKeys() {
return preKeys;
}
public DeviceKey getDeviceKey() {
return deviceKey;
}
public String getIdentityKey() {
return identityKey;
}
public PreKeyV2 getLastResortKey() {
return lastResortKey;
}
}

View File

@ -1,5 +1,5 @@
/** /**
* Copyright (C) 2013 Open WhisperSystems * Copyright (C) 2014 Open Whisper Systems
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU Affero General Public License as published by
@ -17,23 +17,14 @@
package org.whispersystems.textsecuregcm.entities; package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.XmlTransient;
import java.io.Serializable;
@JsonInclude(JsonInclude.Include.NON_DEFAULT) @JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class PreKey { public class PreKeyV1 implements PreKeyBase {
@JsonIgnore
private long id;
@JsonIgnore
private String number;
@JsonProperty @JsonProperty
private long deviceId; private long deviceId;
@ -50,89 +41,43 @@ public class PreKey {
@NotNull @NotNull
private String identityKey; private String identityKey;
@JsonProperty
private boolean lastResort;
@JsonProperty @JsonProperty
private int registrationId; private int registrationId;
public PreKey() {} public PreKeyV1() {}
public PreKey(long id, String number, long deviceId, long keyId, public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey, int registrationId)
String publicKey, boolean lastResort)
{ {
this.id = id; this.deviceId = deviceId;
this.number = number; this.keyId = keyId;
this.deviceId = deviceId; this.publicKey = publicKey;
this.keyId = keyId; this.identityKey = identityKey;
this.publicKey = publicKey; this.registrationId = registrationId;
this.lastResort = lastResort;
} }
@VisibleForTesting @VisibleForTesting
public PreKey(long id, String number, long deviceId, long keyId, public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey)
String publicKey, String identityKey, boolean lastResort)
{ {
this.id = id;
this.number = number;
this.deviceId = deviceId; this.deviceId = deviceId;
this.keyId = keyId; this.keyId = keyId;
this.publicKey = publicKey; this.publicKey = publicKey;
this.identityKey = identityKey; this.identityKey = identityKey;
this.lastResort = lastResort;
}
@XmlTransient
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@XmlTransient
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
} }
@Override
public String getPublicKey() { public String getPublicKey() {
return publicKey; return publicKey;
} }
public void setPublicKey(String publicKey) { @Override
this.publicKey = publicKey;
}
public long getKeyId() { public long getKeyId() {
return keyId; return keyId;
} }
public void setKeyId(long keyId) {
this.keyId = keyId;
}
public String getIdentityKey() { public String getIdentityKey() {
return identityKey; return identityKey;
} }
public void setIdentityKey(String identityKey) {
this.identityKey = identityKey;
}
@XmlTransient
public boolean isLastResort() {
return lastResort;
}
public void setLastResort(boolean lastResort) {
this.lastResort = lastResort;
}
public void setDeviceId(long deviceId) { public void setDeviceId(long deviceId) {
this.deviceId = deviceId; this.deviceId = deviceId;
} }

View File

@ -0,0 +1,82 @@
package org.whispersystems.textsecuregcm.entities;
/**
* Copyright (C) 2014 Open Whisper Systems
*
* 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/>.
*/
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class PreKeyV2 implements PreKeyBase {
@JsonProperty
@NotNull
private long keyId;
@JsonProperty
@NotEmpty
private String publicKey;
public PreKeyV2() {}
public PreKeyV2(long keyId, String publicKey)
{
this.keyId = keyId;
this.publicKey = publicKey;
}
@Override
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
@Override
public long getKeyId() {
return keyId;
}
public void setKeyId(long keyId) {
this.keyId = keyId;
}
@Override
public boolean equals(Object object) {
if (object == null || !(object instanceof PreKeyV2)) return false;
PreKeyV2 that = (PreKeyV2)object;
if (publicKey == null) {
return this.keyId == that.keyId && that.publicKey == null;
} else {
return this.keyId == that.keyId && this.publicKey.equals(that.publicKey);
}
}
@Override
public int hashCode() {
if (publicKey == null) {
return (int)this.keyId;
} else {
return ((int)this.keyId) ^ publicKey.hashCode();
}
}
}

View File

@ -36,7 +36,8 @@ import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContacts; import org.whispersystems.textsecuregcm.entities.ClientContacts;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList; import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.util.Base64; import org.whispersystems.textsecuregcm.util.Base64;
import javax.net.ssl.SSLContext; import javax.net.ssl.SSLContext;
@ -62,11 +63,12 @@ public class FederatedClient {
private final Logger logger = LoggerFactory.getLogger(FederatedClient.class); private final Logger logger = LoggerFactory.getLogger(FederatedClient.class);
private static final String USER_COUNT_PATH = "/v1/federation/user_count"; private static final String USER_COUNT_PATH = "/v1/federation/user_count";
private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d"; private static final String USER_TOKENS_PATH = "/v1/federation/user_tokens/%d";
private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s"; private static final String RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s";
private static final String PREKEY_PATH_DEVICE = "/v1/federation/key/%s/%s"; private static final String PREKEY_PATH_DEVICE_V1 = "/v1/federation/key/%s/%s";
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d"; private static final String PREKEY_PATH_DEVICE_V2 = "/v2/federation/key/%s/%s";
private static final String ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
private final FederatedPeer peer; private final FederatedPeer peer;
private final Client client; private final Client client;
@ -107,9 +109,9 @@ public class FederatedClient {
} }
} }
public Optional<UnstructuredPreKeyList> getKeys(String destination, String device) { public Optional<PreKeyResponseV1> getKeysV1(String destination, String device) {
try { try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE, destination, device)); WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V1, destination, device));
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON) ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader) .header("Authorization", authorizationHeader)
@ -119,7 +121,7 @@ public class FederatedClient {
throw new WebApplicationException(clientResponseToResponse(response)); throw new WebApplicationException(clientResponseToResponse(response));
} }
return Optional.of(response.getEntity(UnstructuredPreKeyList.class)); return Optional.of(response.getEntity(PreKeyResponseV1.class));
} catch (UniformInterfaceException | ClientHandlerException e) { } catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("PreKey", e); logger.warn("PreKey", e);
@ -127,6 +129,27 @@ public class FederatedClient {
} }
} }
public Optional<PreKeyResponseV2> getKeysV2(String destination, String device) {
try {
WebResource resource = client.resource(peer.getUrl()).path(String.format(PREKEY_PATH_DEVICE_V2, destination, device));
ClientResponse response = resource.accept(MediaType.APPLICATION_JSON)
.header("Authorization", authorizationHeader)
.get(ClientResponse.class);
if (response.getStatus() < 200 || response.getStatus() >= 300) {
throw new WebApplicationException(clientResponseToResponse(response));
}
return Optional.of(response.getEntity(PreKeyResponseV2.class));
} catch (UniformInterfaceException | ClientHandlerException e) {
logger.warn("PreKey", e);
return Optional.absent();
}
}
public int getUserCount() { public int getUserCount() {
try { try {
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH); WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH);

View File

@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
@Override @Override
public Optional<Device> getAuthenticatedDevice() { public Optional<Device> getAuthenticatedDevice() {
return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0)); return Optional.of(new Device(deviceId, null, null, null, null, null, false, 0, null));
} }
} }

View File

@ -28,7 +28,7 @@ import java.util.List;
public class Account implements Serializable { public class Account implements Serializable {
public static final int MEMCACHE_VERION = 3; public static final int MEMCACHE_VERION = 4;
@JsonIgnore @JsonIgnore
private long id; private long id;

View File

@ -19,6 +19,8 @@ package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials; import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.entities.DeviceKey;
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import java.io.Serializable; import java.io.Serializable;
@ -51,11 +53,15 @@ public class Device implements Serializable {
@JsonProperty @JsonProperty
private int registrationId; private int registrationId;
@JsonProperty
private DeviceKey deviceKey;
public Device() {} public Device() {}
public Device(long id, String authToken, String salt, public Device(long id, String authToken, String salt,
String signalingKey, String gcmId, String apnId, String signalingKey, String gcmId, String apnId,
boolean fetchesMessages, int registrationId) boolean fetchesMessages, int registrationId,
DeviceKey deviceKey)
{ {
this.id = id; this.id = id;
this.authToken = authToken; this.authToken = authToken;
@ -65,6 +71,7 @@ public class Device implements Serializable {
this.apnId = apnId; this.apnId = apnId;
this.fetchesMessages = fetchesMessages; this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId; this.registrationId = registrationId;
this.deviceKey = deviceKey;
} }
public String getApnId() { public String getApnId() {
@ -131,4 +138,12 @@ public class Device implements Serializable {
public void setRegistrationId(int registrationId) { public void setRegistrationId(int registrationId) {
this.registrationId = registrationId; this.registrationId = registrationId;
} }
public DeviceKey getDeviceKey() {
return deviceKey;
}
public void setDeviceKey(DeviceKey deviceKey) {
this.deviceKey = deviceKey;
}
} }

View File

@ -0,0 +1,46 @@
package org.whispersystems.textsecuregcm.storage;
public class KeyRecord {
private long id;
private String number;
private long deviceId;
private long keyId;
private String publicKey;
private boolean lastResort;
public KeyRecord(long id, String number, long deviceId, long keyId,
String publicKey, boolean lastResort)
{
this.id = id;
this.number = number;
this.deviceId = deviceId;
this.keyId = keyId;
this.publicKey = publicKey;
this.lastResort = lastResort;
}
public long getId() {
return id;
}
public String getNumber() {
return number;
}
public long getDeviceId() {
return deviceId;
}
public long getKeyId() {
return keyId;
}
public String getPublicKey() {
return publicKey;
}
public boolean isLastResort() {
return lastResort;
}
}

View File

@ -30,8 +30,9 @@ import org.skife.jdbi.v2.sqlobject.SqlUpdate;
import org.skife.jdbi.v2.sqlobject.Transaction; import org.skife.jdbi.v2.sqlobject.Transaction;
import org.skife.jdbi.v2.sqlobject.customizers.Mapper; import org.skife.jdbi.v2.sqlobject.customizers.Mapper;
import org.skife.jdbi.v2.tweak.ResultSetMapper; import org.skife.jdbi.v2.tweak.ResultSetMapper;
import org.whispersystems.textsecuregcm.entities.PreKey; import org.whispersystems.textsecuregcm.entities.PreKeyBase;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; import org.whispersystems.textsecuregcm.entities.PreKeyV1;
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
@ -40,6 +41,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List; import java.util.List;
public abstract class Keys { public abstract class Keys {
@ -51,65 +53,64 @@ public abstract class Keys {
abstract void removeKey(@Bind("id") long id); abstract void removeKey(@Bind("id") long id);
@SqlBatch("INSERT INTO keys (number, device_id, key_id, public_key, last_resort) VALUES " + @SqlBatch("INSERT INTO keys (number, device_id, key_id, public_key, last_resort) VALUES " +
"(:number, :device_id, :key_id, :public_key, :last_resort)") "(:number, :device_id, :key_id, :public_key, :last_resort)")
abstract void append(@PreKeyBinder List<PreKey> preKeys); abstract void append(@PreKeyBinder List<KeyRecord> preKeys);
@SqlUpdate("INSERT INTO keys (number, device_id, key_id, public_key, last_resort) VALUES " +
"(:number, :device_id, :key_id, :public_key, :last_resort)")
abstract void append(@PreKeyBinder PreKey preKey);
@SqlQuery("SELECT * FROM keys WHERE number = :number AND device_id = :device_id ORDER BY key_id ASC FOR UPDATE") @SqlQuery("SELECT * FROM keys WHERE number = :number AND device_id = :device_id ORDER BY key_id ASC FOR UPDATE")
@Mapper(PreKeyMapper.class) @Mapper(PreKeyMapper.class)
abstract PreKey retrieveFirst(@Bind("number") String number, @Bind("device_id") long deviceId); abstract KeyRecord retrieveFirst(@Bind("number") String number, @Bind("device_id") long deviceId);
@SqlQuery("SELECT DISTINCT ON (number, device_id) * FROM keys WHERE number = :number ORDER BY number, device_id, key_id ASC") @SqlQuery("SELECT DISTINCT ON (number, device_id) * FROM keys WHERE number = :number ORDER BY number, device_id, key_id ASC")
@Mapper(PreKeyMapper.class) @Mapper(PreKeyMapper.class)
abstract List<PreKey> retrieveFirst(@Bind("number") String number); abstract List<KeyRecord> retrieveFirst(@Bind("number") String number);
@SqlQuery("SELECT COUNT(*) FROM keys WHERE number = :number AND device_id = :device_id") @SqlQuery("SELECT COUNT(*) FROM keys WHERE number = :number AND device_id = :device_id")
public abstract int getCount(@Bind("number") String number, @Bind("device_id") long deviceId); public abstract int getCount(@Bind("number") String number, @Bind("device_id") long deviceId);
@Transaction(TransactionIsolationLevel.SERIALIZABLE) @Transaction(TransactionIsolationLevel.SERIALIZABLE)
public void store(String number, long deviceId, List<PreKey> keys, PreKey lastResortKey) { public void store(String number, long deviceId, List<? extends PreKeyBase> keys, PreKeyBase lastResortKey) {
for (PreKey key : keys) { List<KeyRecord> records = new LinkedList<>();
key.setNumber(number);
key.setDeviceId(deviceId); for (PreKeyBase key : keys) {
records.add(new KeyRecord(0, number, deviceId, key.getKeyId(), key.getPublicKey(), false));
} }
lastResortKey.setNumber(number); records.add(new KeyRecord(0, number, deviceId, lastResortKey.getKeyId(),
lastResortKey.setDeviceId(deviceId); lastResortKey.getPublicKey(), true));
lastResortKey.setLastResort(true);
removeKeys(number, deviceId); removeKeys(number, deviceId);
append(keys); append(records);
append(lastResortKey);
} }
@Transaction(TransactionIsolationLevel.SERIALIZABLE) @Transaction(TransactionIsolationLevel.SERIALIZABLE)
public Optional<UnstructuredPreKeyList> get(String number, long deviceId) { public Optional<List<KeyRecord>> get(String number, long deviceId) {
PreKey preKey = retrieveFirst(number, deviceId); final KeyRecord record = retrieveFirst(number, deviceId);
if (preKey != null && !preKey.isLastResort()) { if (record != null && !record.isLastResort()) {
removeKey(preKey.getId()); removeKey(record.getId());
} else if (record == null) {
return Optional.absent();
} }
if (preKey != null) return Optional.of(new UnstructuredPreKeyList(preKey)); List<KeyRecord> results = new LinkedList<>();
else return Optional.absent(); results.add(record);
return Optional.of(results);
} }
@Transaction(TransactionIsolationLevel.SERIALIZABLE) @Transaction(TransactionIsolationLevel.SERIALIZABLE)
public Optional<UnstructuredPreKeyList> get(String number) { public Optional<List<KeyRecord>> get(String number) {
List<PreKey> preKeys = retrieveFirst(number); List<KeyRecord> preKeys = retrieveFirst(number);
if (preKeys != null) { if (preKeys != null) {
for (PreKey preKey : preKeys) { for (KeyRecord preKey : preKeys) {
if (!preKey.isLastResort()) { if (!preKey.isLastResort()) {
removeKey(preKey.getId()); removeKey(preKey.getId());
} }
} }
} }
if (preKeys != null) return Optional.of(new UnstructuredPreKeyList(preKeys)); if (preKeys != null) return Optional.of(preKeys);
else return Optional.absent(); else return Optional.absent();
} }
@ -120,16 +121,16 @@ public abstract class Keys {
public static class PreKeyBinderFactory implements BinderFactory { public static class PreKeyBinderFactory implements BinderFactory {
@Override @Override
public Binder build(Annotation annotation) { public Binder build(Annotation annotation) {
return new Binder<PreKeyBinder, PreKey>() { return new Binder<PreKeyBinder, KeyRecord>() {
@Override @Override
public void bind(SQLStatement<?> sql, PreKeyBinder accountBinder, PreKey preKey) public void bind(SQLStatement<?> sql, PreKeyBinder accountBinder, KeyRecord record)
{ {
sql.bind("id", preKey.getId()); sql.bind("id", record.getId());
sql.bind("number", preKey.getNumber()); sql.bind("number", record.getNumber());
sql.bind("device_id", preKey.getDeviceId()); sql.bind("device_id", record.getDeviceId());
sql.bind("key_id", preKey.getKeyId()); sql.bind("key_id", record.getKeyId());
sql.bind("public_key", preKey.getPublicKey()); sql.bind("public_key", record.getPublicKey());
sql.bind("last_resort", preKey.isLastResort() ? 1 : 0); sql.bind("last_resort", record.isLastResort() ? 1 : 0);
} }
}; };
} }
@ -137,14 +138,14 @@ public abstract class Keys {
} }
public static class PreKeyMapper implements ResultSetMapper<PreKey> { public static class PreKeyMapper implements ResultSetMapper<KeyRecord> {
@Override @Override
public PreKey map(int i, ResultSet resultSet, StatementContext statementContext) public KeyRecord map(int i, ResultSet resultSet, StatementContext statementContext)
throws SQLException throws SQLException
{ {
return new PreKey(resultSet.getLong("id"), resultSet.getString("number"), resultSet.getLong("device_id"), return new KeyRecord(resultSet.getLong("id"), resultSet.getString("number"),
resultSet.getLong("key_id"), resultSet.getString("public_key"), resultSet.getLong("device_id"), resultSet.getLong("key_id"),
resultSet.getInt("last_resort") == 1); resultSet.getString("public_key"), resultSet.getInt("last_resort") == 1);
} }
} }

View File

@ -52,7 +52,7 @@ public class FederatedControllerTest {
public final ResourceTestRule resources = ResourceTestRule.builder() public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator()) .addProvider(AuthHelper.getAuthenticator())
.addResource(new FederationController(accountsManager, .addResource(new FederationController(accountsManager,
null, null, null, null, null,
messageController)) messageController))
.build(); .build();
@ -61,12 +61,12 @@ public class FederatedControllerTest {
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
List<Device> singleDeviceList = new LinkedList<Device>() {{ List<Device> singleDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111)); add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
}}; }};
List<Device> multiDeviceList = new LinkedList<Device>() {{ List<Device> multiDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222)); add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333)); add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
}}; }};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList); Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);

View File

@ -6,18 +6,22 @@ import org.junit.Before;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock; import org.whispersystems.textsecuregcm.controllers.KeysControllerV1;
import org.mockito.stubbing.Answer; import org.whispersystems.textsecuregcm.controllers.KeysControllerV2;
import org.whispersystems.textsecuregcm.controllers.KeysController; import org.whispersystems.textsecuregcm.entities.DeviceKey;
import org.whispersystems.textsecuregcm.entities.PreKey; import org.whispersystems.textsecuregcm.entities.PreKeyCount;
import org.whispersystems.textsecuregcm.entities.PreKeyList; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
import org.whispersystems.textsecuregcm.entities.PreKeyStatus; import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList; import org.whispersystems.textsecuregcm.entities.PreKeyStateV1;
import org.whispersystems.textsecuregcm.entities.PreKeyStateV2;
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.KeyRecord;
import org.whispersystems.textsecuregcm.storage.Keys; import org.whispersystems.textsecuregcm.storage.Keys;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
@ -36,10 +40,18 @@ public class KeyControllerTest {
private static int SAMPLE_REGISTRATION_ID = 999; private static int SAMPLE_REGISTRATION_ID = 999;
private static int SAMPLE_REGISTRATION_ID2 = 1002; private static int SAMPLE_REGISTRATION_ID2 = 1002;
private static int SAMPLE_REGISTRATION_ID4 = 1555;
private final KeyRecord SAMPLE_KEY = new KeyRecord(1, EXISTS_NUMBER, Device.MASTER_ID, 1234, "test1", false);
private final KeyRecord SAMPLE_KEY2 = new KeyRecord(2, EXISTS_NUMBER, 2, 5667, "test3", false );
private final KeyRecord SAMPLE_KEY3 = new KeyRecord(3, EXISTS_NUMBER, 3, 334, "test5", false );
private final KeyRecord SAMPLE_KEY4 = new KeyRecord(4, EXISTS_NUMBER, 4, 336, "test6", false );
private final DeviceKey SAMPLE_DEVICE_KEY = new DeviceKey(1111, "foofoo", "sig11");
private final DeviceKey SAMPLE_DEVICE_KEY2 = new DeviceKey(2222, "foobar", "sig22");
private final DeviceKey SAMPLE_DEVICE_KEY3 = new DeviceKey(3333, "barfoo", "sig33");
private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, Device.MASTER_ID, 1234, "test1", "test2", false);
private final PreKey SAMPLE_KEY2 = new PreKey(2, EXISTS_NUMBER, 2, 5667, "test3", "test4,", false );
private final PreKey SAMPLE_KEY3 = new PreKey(3, EXISTS_NUMBER, 3, 334, "test5", "test6", false );
private final Keys keys = mock(Keys.class ); private final Keys keys = mock(Keys.class );
private final AccountsManager accounts = mock(AccountsManager.class); private final AccountsManager accounts = mock(AccountsManager.class);
private final Account existsAccount = mock(Account.class ); private final Account existsAccount = mock(Account.class );
@ -50,25 +62,47 @@ public class KeyControllerTest {
@Rule @Rule
public final ResourceTestRule resources = ResourceTestRule.builder() public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthenticator()) .addProvider(AuthHelper.getAuthenticator())
.addResource(new KeysController(rateLimiters, keys, accounts, null)) .addResource(new KeysControllerV1(rateLimiters, keys, accounts, null))
.addResource(new KeysControllerV2(rateLimiters, keys, accounts, null))
.build(); .build();
@Before @Before
public void setup() { public void setup() {
Device sampleDevice = mock(Device.class ); final Device sampleDevice = mock(Device.class );
Device sampleDevice2 = mock(Device.class); final Device sampleDevice2 = mock(Device.class);
Device sampleDevice3 = mock(Device.class); final Device sampleDevice3 = mock(Device.class);
final Device sampleDevice4 = mock(Device.class);
List<Device> allDevices = new LinkedList<Device>() {{
add(sampleDevice);
add(sampleDevice2);
add(sampleDevice3);
add(sampleDevice4);
}};
when(sampleDevice.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID); when(sampleDevice.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID);
when(sampleDevice2.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2); when(sampleDevice2.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2);
when(sampleDevice3.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2); when(sampleDevice3.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2);
when(sampleDevice4.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID4);
when(sampleDevice.isActive()).thenReturn(true); when(sampleDevice.isActive()).thenReturn(true);
when(sampleDevice2.isActive()).thenReturn(true); when(sampleDevice2.isActive()).thenReturn(true);
when(sampleDevice3.isActive()).thenReturn(false); when(sampleDevice3.isActive()).thenReturn(false);
when(sampleDevice4.isActive()).thenReturn(true);
when(sampleDevice.getDeviceKey()).thenReturn(SAMPLE_DEVICE_KEY);
when(sampleDevice2.getDeviceKey()).thenReturn(SAMPLE_DEVICE_KEY2);
when(sampleDevice3.getDeviceKey()).thenReturn(SAMPLE_DEVICE_KEY3);
when(sampleDevice4.getDeviceKey()).thenReturn(null);
when(sampleDevice.getId()).thenReturn(1L);
when(sampleDevice2.getId()).thenReturn(2L);
when(sampleDevice3.getId()).thenReturn(3L);
when(sampleDevice4.getId()).thenReturn(4L);
when(existsAccount.getDevice(1L)).thenReturn(Optional.of(sampleDevice)); when(existsAccount.getDevice(1L)).thenReturn(Optional.of(sampleDevice));
when(existsAccount.getDevice(2L)).thenReturn(Optional.of(sampleDevice2)); when(existsAccount.getDevice(2L)).thenReturn(Optional.of(sampleDevice2));
when(existsAccount.getDevice(3L)).thenReturn(Optional.of(sampleDevice3)); when(existsAccount.getDevice(3L)).thenReturn(Optional.of(sampleDevice3));
when(existsAccount.getDevice(4L)).thenReturn(Optional.of(sampleDevice4));
when(existsAccount.getDevice(22L)).thenReturn(Optional.<Device>absent());
when(existsAccount.getDevices()).thenReturn(allDevices);
when(existsAccount.isActive()).thenReturn(true); when(existsAccount.isActive()).thenReturn(true);
when(existsAccount.getIdentityKey()).thenReturn("existsidentitykey"); when(existsAccount.getIdentityKey()).thenReturn("existsidentitykey");
@ -77,86 +111,155 @@ public class KeyControllerTest {
when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter); when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter);
when(keys.get(eq(EXISTS_NUMBER), eq(1L))).thenAnswer(new Answer<Optional<UnstructuredPreKeyList>>() { List<KeyRecord> singleDevice = new LinkedList<>();
@Override singleDevice.add(SAMPLE_KEY);
public Optional<UnstructuredPreKeyList> answer(InvocationOnMock invocationOnMock) throws Throwable { when(keys.get(eq(EXISTS_NUMBER), eq(1L))).thenReturn(Optional.of(singleDevice));
return Optional.of(new UnstructuredPreKeyList(cloneKey(SAMPLE_KEY)));
}
});
when(keys.get(eq(NOT_EXISTS_NUMBER), eq(1L))).thenReturn(Optional.<UnstructuredPreKeyList>absent()); when(keys.get(eq(NOT_EXISTS_NUMBER), eq(1L))).thenReturn(Optional.<List<KeyRecord>>absent());
when(keys.get(EXISTS_NUMBER)).thenAnswer(new Answer<Optional<UnstructuredPreKeyList>>() { List<KeyRecord> multiDevice = new LinkedList<>();
@Override multiDevice.add(SAMPLE_KEY);
public Optional<UnstructuredPreKeyList> answer(InvocationOnMock invocationOnMock) throws Throwable { multiDevice.add(SAMPLE_KEY2);
List<PreKey> allKeys = new LinkedList<>(); multiDevice.add(SAMPLE_KEY3);
allKeys.add(cloneKey(SAMPLE_KEY)); multiDevice.add(SAMPLE_KEY4);
allKeys.add(cloneKey(SAMPLE_KEY2)); when(keys.get(EXISTS_NUMBER)).thenReturn(Optional.of(multiDevice));
allKeys.add(cloneKey(SAMPLE_KEY3));
return Optional.of(new UnstructuredPreKeyList(allKeys));
}
});
when(keys.getCount(eq(AuthHelper.VALID_NUMBER), eq(1L))).thenReturn(5); when(keys.getCount(eq(AuthHelper.VALID_NUMBER), eq(1L))).thenReturn(5);
when(AuthHelper.VALID_DEVICE.getDeviceKey()).thenReturn(new DeviceKey(89898, "zoofarb", "sigvalid"));
when(AuthHelper.VALID_ACCOUNT.getIdentityKey()).thenReturn(null); when(AuthHelper.VALID_ACCOUNT.getIdentityKey()).thenReturn(null);
} }
@Test @Test
public void validKeyStatusTest() throws Exception { public void validKeyStatusTestV1() throws Exception {
PreKeyStatus result = resources.client().resource("/v1/keys") PreKeyCount result = resources.client().resource("/v1/keys")
.header("Authorization", .header("Authorization",
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyStatus.class); .get(PreKeyCount.class);
assertThat(result.getCount() == 4); assertThat(result.getCount() == 4);
verify(keys).getCount(eq(AuthHelper.VALID_NUMBER), eq(1L)); verify(keys).getCount(eq(AuthHelper.VALID_NUMBER), eq(1L));
} }
@Test
public void validKeyStatusTestV2() throws Exception {
PreKeyCount result = resources.client().resource("/v2/keys")
.header("Authorization",
AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyCount.class);
assertThat(result.getCount() == 4);
verify(keys).getCount(eq(AuthHelper.VALID_NUMBER), eq(1L));
}
@Test @Test
public void validLegacyRequestTest() throws Exception { public void validLegacyRequestTest() throws Exception {
PreKey result = resources.client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER)) PreKeyV1 result = resources.client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKey.class); .get(PreKeyV1.class);
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId()); assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey()); assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey()); assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
assertThat(result.getId() == 0);
assertThat(result.getNumber() == null);
verify(keys).get(eq(EXISTS_NUMBER), eq(1L)); verify(keys).get(eq(EXISTS_NUMBER), eq(1L));
verifyNoMoreInteractions(keys); verifyNoMoreInteractions(keys);
} }
@Test @Test
public void validMultiRequestTest() throws Exception { public void validSingleRequestTestV2() throws Exception {
UnstructuredPreKeyList results = resources.client().resource(String.format("/v1/keys/%s/*", EXISTS_NUMBER)) PreKeyResponseV2 result = resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyResponseV2.class);
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
assertThat(result.getDevices().size()).isEqualTo(1);
assertThat(result.getDevices().get(0).getPreKey().getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
assertThat(result.getDevices().get(0).getPreKey().getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
assertThat(result.getDevices().get(0).getDeviceKey()).isEqualTo(existsAccount.getDevice(1).get().getDeviceKey());
verify(keys).get(eq(EXISTS_NUMBER), eq(1L));
verifyNoMoreInteractions(keys);
}
@Test
public void validMultiRequestTestV1() throws Exception {
PreKeyResponseV1 results = resources.client().resource(String.format("/v1/keys/%s/*", EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(UnstructuredPreKeyList.class); .get(PreKeyResponseV1.class);
assertThat(results.getKeys().size()).isEqualTo(2); assertThat(results.getKeys().size()).isEqualTo(3);
PreKey result = results.getKeys().get(0); PreKeyV1 result = results.getKeys().get(0);
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId()); assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey()); assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey()); assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID); assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID);
assertThat(result.getId() == 0);
assertThat(result.getNumber() == null);
result = results.getKeys().get(1); result = results.getKeys().get(1);
assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId()); assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId());
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey()); assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey());
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey()); assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID2); assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID2);
assertThat(result.getId() == 0); result = results.getKeys().get(2);
assertThat(result.getNumber() == null); assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY4.getKeyId());
assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY4.getPublicKey());
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID4);
verify(keys).get(eq(EXISTS_NUMBER));
verifyNoMoreInteractions(keys);
}
@Test
public void validMultiRequestTestV2() throws Exception {
PreKeyResponseV2 results = resources.client().resource(String.format("/v2/keys/%s/*", EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(PreKeyResponseV2.class);
assertThat(results.getDevices().size()).isEqualTo(3);
assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
PreKeyV2 deviceKey = results.getDevices().get(0).getDeviceKey();
PreKeyV2 preKey = results.getDevices().get(0).getPreKey();
long registrationId = results.getDevices().get(0).getRegistrationId();
long deviceId = results.getDevices().get(0).getDeviceId();
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID);
assertThat(deviceKey.getKeyId()).isEqualTo(SAMPLE_DEVICE_KEY.getKeyId());
assertThat(deviceKey.getPublicKey()).isEqualTo(SAMPLE_DEVICE_KEY.getPublicKey());
assertThat(deviceId).isEqualTo(1);
deviceKey = results.getDevices().get(1).getDeviceKey();
preKey = results.getDevices().get(1).getPreKey();
registrationId = results.getDevices().get(1).getRegistrationId();
deviceId = results.getDevices().get(1).getDeviceId();
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId());
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey());
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID2);
assertThat(deviceKey.getKeyId()).isEqualTo(SAMPLE_DEVICE_KEY2.getKeyId());
assertThat(deviceKey.getPublicKey()).isEqualTo(SAMPLE_DEVICE_KEY2.getPublicKey());
assertThat(deviceId).isEqualTo(2);
deviceKey = results.getDevices().get(2).getDeviceKey();
preKey = results.getDevices().get(2).getPreKey();
registrationId = results.getDevices().get(2).getRegistrationId();
deviceId = results.getDevices().get(2).getDeviceId();
assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY4.getKeyId());
assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY4.getPublicKey());
assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID4);
assertThat(deviceKey).isNull();
assertThat(deviceId).isEqualTo(4);
verify(keys).get(eq(EXISTS_NUMBER)); verify(keys).get(eq(EXISTS_NUMBER));
verifyNoMoreInteractions(keys); verifyNoMoreInteractions(keys);
@ -164,7 +267,7 @@ public class KeyControllerTest {
@Test @Test
public void invalidRequestTest() throws Exception { public void invalidRequestTestV1() throws Exception {
ClientResponse response = resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER)) ClientResponse response = resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(ClientResponse.class); .get(ClientResponse.class);
@ -173,7 +276,25 @@ public class KeyControllerTest {
} }
@Test @Test
public void unauthorizedRequestTest() throws Exception { public void invalidRequestTestV2() throws Exception {
ClientResponse response = resources.client().resource(String.format("/v2/keys/%s", NOT_EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(ClientResponse.class);
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404);
}
@Test
public void anotherInvalidRequestTestV2() throws Exception {
ClientResponse response = resources.client().resource(String.format("/v2/keys/%s/22", EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(ClientResponse.class);
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404);
}
@Test
public void unauthorizedRequestTestV1() throws Exception {
ClientResponse response = ClientResponse response =
resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER)) resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
@ -189,15 +310,31 @@ public class KeyControllerTest {
} }
@Test @Test
public void putKeysTest() throws Exception { public void unauthorizedRequestTestV2() throws Exception {
final PreKey newKey = new PreKey(0, null, 1L, 31337, "foobar", "foobarbaz", false); ClientResponse response =
final PreKey lastResortKey = new PreKey(0, null, 1L, 0xFFFFFF, "fooz", "foobarbaz", false); resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
.get(ClientResponse.class);
List<PreKey> preKeys = new LinkedList<PreKey>() {{ assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
response =
resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
.get(ClientResponse.class);
assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401);
}
@Test
public void putKeysTestV1() throws Exception {
final PreKeyV1 newKey = new PreKeyV1(1L, 31337, "foobar", "foobarbaz");
final PreKeyV1 lastResortKey = new PreKeyV1(1L, 0xFFFFFF, "fooz", "foobarbaz");
List<PreKeyV1> preKeys = new LinkedList<PreKeyV1>() {{
add(newKey); add(newKey);
}}; }};
PreKeyList preKeyList = new PreKeyList(); PreKeyStateV1 preKeyList = new PreKeyStateV1();
preKeyList.setKeys(preKeys); preKeyList.setKeys(preKeys);
preKeyList.setLastResortKey(lastResortKey); preKeyList.setLastResortKey(lastResortKey);
@ -209,11 +346,11 @@ public class KeyControllerTest {
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(204); assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(204);
ArgumentCaptor<List> listCaptor = ArgumentCaptor.forClass(List.class ); ArgumentCaptor<List> listCaptor = ArgumentCaptor.forClass(List.class );
ArgumentCaptor<PreKey> lastResortCaptor = ArgumentCaptor.forClass(PreKey.class); ArgumentCaptor<PreKeyV1> lastResortCaptor = ArgumentCaptor.forClass(PreKeyV1.class);
verify(keys).store(eq(AuthHelper.VALID_NUMBER), eq(1L), listCaptor.capture(), lastResortCaptor.capture()); verify(keys).store(eq(AuthHelper.VALID_NUMBER), eq(1L), listCaptor.capture(), lastResortCaptor.capture());
List<PreKey> capturedList = listCaptor.getValue(); List<PreKeyV1> capturedList = listCaptor.getValue();
assertThat(capturedList.size() == 1); assertThat(capturedList.size() == 1);
assertThat(capturedList.get(0).getIdentityKey().equals("foobarbaz")); assertThat(capturedList.get(0).getIdentityKey().equals("foobarbaz"));
assertThat(capturedList.get(0).getKeyId() == 31337); assertThat(capturedList.get(0).getKeyId() == 31337);
@ -226,9 +363,39 @@ public class KeyControllerTest {
verify(accounts).update(AuthHelper.VALID_ACCOUNT); verify(accounts).update(AuthHelper.VALID_ACCOUNT);
} }
private PreKey cloneKey(PreKey source) { @Test
return new PreKey(source.getId(), source.getNumber(), source.getDeviceId(), source.getKeyId(), public void putKeysTestV2() throws Exception {
source.getPublicKey(), source.getIdentityKey(), source.isLastResort()); final PreKeyV2 preKey = new PreKeyV2(31337, "foobar");
final PreKeyV2 lastResortKey = new PreKeyV2(31339, "barbar");
final DeviceKey deviceKey = new DeviceKey(31338, "foobaz", "myvalidsig");
final String identityKey = "barbar";
List<PreKeyV2> preKeys = new LinkedList<PreKeyV2>() {{
add(preKey);
}};
PreKeyStateV2 preKeyState = new PreKeyStateV2(identityKey, deviceKey, preKeys, lastResortKey);
ClientResponse response =
resources.client().resource("/v2/keys")
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.type(MediaType.APPLICATION_JSON_TYPE)
.put(ClientResponse.class, preKeyState);
assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(204);
ArgumentCaptor<List> listCaptor = ArgumentCaptor.forClass(List.class);
verify(keys).store(eq(AuthHelper.VALID_NUMBER), eq(1L), listCaptor.capture(), eq(lastResortKey));
List<PreKeyV2> capturedList = listCaptor.getValue();
assertThat(capturedList.size() == 1);
assertThat(capturedList.get(0).getKeyId() == 31337);
assertThat(capturedList.get(0).getPublicKey().equals("foobar"));
verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq("barbar"));
verify(AuthHelper.VALID_DEVICE).setDeviceKey(eq(deviceKey));
verify(accounts).update(AuthHelper.VALID_ACCOUNT);
} }
} }

View File

@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.ClientResponse;
import org.junit.Before; import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.whispersystems.textsecuregcm.controllers.MessageController; import org.whispersystems.textsecuregcm.controllers.MessageController;
@ -59,12 +58,12 @@ public class MessageControllerTest {
@Before @Before
public void setup() throws Exception { public void setup() throws Exception {
List<Device> singleDeviceList = new LinkedList<Device>() {{ List<Device> singleDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111)); add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null));
}}; }};
List<Device> multiDeviceList = new LinkedList<Device>() {{ List<Device> multiDeviceList = new LinkedList<Device>() {{
add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222)); add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null));
add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333)); add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null));
}}; }};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList); Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList);

View File

@ -2,7 +2,8 @@ package org.whispersystems.textsecuregcm.tests.entities;
import org.junit.Test; import org.junit.Test;
import org.whispersystems.textsecuregcm.entities.ClientContact; import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.PreKey; import org.whispersystems.textsecuregcm.entities.PreKeyV1;
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.util.Util;
import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.equalTo;
@ -13,8 +14,8 @@ import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.*;
public class PreKeyTest { public class PreKeyTest {
@Test @Test
public void serializeToJSON() throws Exception { public void serializeToJSONV1() throws Exception {
PreKey preKey = new PreKey(1, "+14152222222", 1, 1234, "test", "identityTest", false); PreKeyV1 preKey = new PreKeyV1(1, 1234, "test", "identityTest");
preKey.setRegistrationId(987); preKey.setRegistrationId(987);
assertThat("Basic Contact Serialization works", assertThat("Basic Contact Serialization works",
@ -23,7 +24,7 @@ public class PreKeyTest {
} }
@Test @Test
public void deserializeFromJSON() throws Exception { public void deserializeFromJSONV() throws Exception {
ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"), ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"),
"whisper", true); "whisper", true);
@ -32,4 +33,13 @@ public class PreKeyTest {
is(contact)); is(contact));
} }
@Test
public void serializeToJSONV2() throws Exception {
PreKeyV2 preKey = new PreKeyV2(1234, "test");
assertThat("PreKeyV2 Serialization works",
asJson(preKey),
is(equalTo(jsonFixture("fixtures/prekey_v2.json"))));
}
} }

View File

@ -0,0 +1,4 @@
{
"keyId" : 1234,
"publicKey" : "test"
}