mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-25 13:48:06 +01:00
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:
@@ -33,7 +33,8 @@ import org.whispersystems.textsecuregcm.controllers.AttachmentController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
|
||||
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.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
@@ -147,7 +148,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
accountsManager);
|
||||
|
||||
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);
|
||||
|
||||
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 DeviceController(pendingDevicesManager, accountsManager, rateLimiters));
|
||||
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(keysController);
|
||||
environment.jersey().register(keysControllerV1);
|
||||
environment.jersey().register(keysControllerV2);
|
||||
environment.jersey().register(messageController);
|
||||
|
||||
if (config.getWebsocketConfiguration().isEnabled()) {
|
||||
|
||||
@@ -25,8 +25,9 @@ import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
@@ -46,7 +47,7 @@ import java.util.List;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@Path("/v1/federation")
|
||||
@Path("/")
|
||||
public class FederationController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FederationController.class);
|
||||
@@ -55,23 +56,26 @@ public class FederationController {
|
||||
|
||||
private final AccountsManager accounts;
|
||||
private final AttachmentController attachmentController;
|
||||
private final KeysController keysController;
|
||||
private final KeysControllerV1 keysControllerV1;
|
||||
private final KeysControllerV2 keysControllerV2;
|
||||
private final MessageController messageController;
|
||||
|
||||
public FederationController(AccountsManager accounts,
|
||||
AttachmentController attachmentController,
|
||||
KeysController keysController,
|
||||
MessageController messageController)
|
||||
KeysControllerV1 keysControllerV1,
|
||||
KeysControllerV2 keysControllerV2,
|
||||
MessageController messageController)
|
||||
{
|
||||
this.accounts = accounts;
|
||||
this.attachmentController = attachmentController;
|
||||
this.keysController = keysController;
|
||||
this.keysControllerV1 = keysControllerV1;
|
||||
this.keysControllerV2 = keysControllerV2;
|
||||
this.messageController = messageController;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/attachment/{attachmentId}")
|
||||
@Path("/v1/federation/attachment/{attachmentId}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
|
||||
@PathParam("attachmentId") long attachmentId)
|
||||
@@ -83,14 +87,15 @@ public class FederationController {
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/key/{number}")
|
||||
@Path("/v1/federation/key/{number}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public PreKey getKey(@Auth FederatedPeer peer,
|
||||
@PathParam("number") String number)
|
||||
public Optional<PreKeyV1> getKey(@Auth FederatedPeer peer,
|
||||
@PathParam("number") String number)
|
||||
throws IOException
|
||||
{
|
||||
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) {
|
||||
logger.warn("Rate limiting on federated channel", e);
|
||||
throw new IOException(e);
|
||||
@@ -99,16 +104,34 @@ public class FederationController {
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/key/{number}/{device}")
|
||||
@Path("/v1/federation/key/{number}/{device}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public UnstructuredPreKeyList getKeys(@Auth FederatedPeer peer,
|
||||
@PathParam("number") String number,
|
||||
@PathParam("device") String device)
|
||||
public Optional<PreKeyResponseV1> getKeysV1(@Auth FederatedPeer peer,
|
||||
@PathParam("number") String number,
|
||||
@PathParam("device") String device)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
return keysController.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()),
|
||||
number, device, Optional.<String>absent());
|
||||
return keysControllerV1.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()),
|
||||
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) {
|
||||
logger.warn("Rate limiting on federated channel", e);
|
||||
throw new IOException(e);
|
||||
@@ -117,7 +140,7 @@ public class FederationController {
|
||||
|
||||
@Timed
|
||||
@PUT
|
||||
@Path("/messages/{source}/{sourceDeviceId}/{destination}")
|
||||
@Path("/v1/federation/messages/{source}/{sourceDeviceId}/{destination}")
|
||||
public void sendMessages(@Auth FederatedPeer peer,
|
||||
@PathParam("source") String source,
|
||||
@PathParam("sourceDeviceId") long sourceDeviceId,
|
||||
@@ -136,7 +159,7 @@ public class FederationController {
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/user_count")
|
||||
@Path("/v1/federation/user_count")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public AccountCount getUserCount(@Auth FederatedPeer peer) {
|
||||
return new AccountCount((int)accounts.getCount());
|
||||
@@ -144,7 +167,7 @@ public class FederationController {
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/user_tokens/{offset}")
|
||||
@Path("/v1/federation/user_tokens/{offset}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
|
||||
@PathParam("offset") int offset)
|
||||
|
||||
@@ -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
|
||||
* 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.google.common.base.Optional;
|
||||
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.PreKeyStatus;
|
||||
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
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 KeysController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(KeysController.class);
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final Keys keys;
|
||||
private final AccountsManager accounts;
|
||||
private final FederatedClientManager federatedClientManager;
|
||||
protected final RateLimiters rateLimiters;
|
||||
protected final Keys keys;
|
||||
protected final AccountsManager accounts;
|
||||
protected final FederatedClientManager federatedClientManager;
|
||||
|
||||
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
|
||||
FederatedClientManager federatedClientManager)
|
||||
@@ -67,119 +52,65 @@ public class KeysController {
|
||||
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
|
||||
@GET
|
||||
@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());
|
||||
|
||||
if (count > 0) {
|
||||
count = count - 1;
|
||||
}
|
||||
|
||||
return new PreKeyStatus(count);
|
||||
return new PreKeyCount(count);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@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
|
||||
protected TargetKeys getLocalKeys(String number, String deviceIdSelector)
|
||||
throws NoSuchUserException
|
||||
{
|
||||
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);
|
||||
|
||||
if (!destination.isPresent() || !destination.get().isActive()) {
|
||||
return Optional.absent();
|
||||
throw new NoSuchUserException("Target account is inactive");
|
||||
}
|
||||
|
||||
try {
|
||||
if (deviceIdSelector.equals("*")) {
|
||||
Optional<UnstructuredPreKeyList> preKeys = keys.get(number);
|
||||
return getActiveKeys(destination.get(), preKeys);
|
||||
Optional<List<KeyRecord>> preKeys = keys.get(number);
|
||||
return new TargetKeys(destination.get(), preKeys);
|
||||
}
|
||||
|
||||
long deviceId = Long.parseLong(deviceIdSelector);
|
||||
Optional<Device> targetDevice = destination.get().getDevice(deviceId);
|
||||
|
||||
if (!targetDevice.isPresent() || !targetDevice.get().isActive()) {
|
||||
return Optional.absent();
|
||||
throw new NoSuchUserException("Target device is inactive.");
|
||||
}
|
||||
|
||||
Optional<UnstructuredPreKeyList> preKeys = keys.get(number, deviceId);
|
||||
return getActiveKeys(destination.get(), preKeys);
|
||||
Optional<List<KeyRecord>> preKeys = keys.get(number, deviceId);
|
||||
return new TargetKeys(destination.get(), preKeys);
|
||||
} catch (NumberFormatException e) {
|
||||
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()) {
|
||||
Optional<Device> device = destination.getDevice(preKey.getDeviceId());
|
||||
|
||||
if (device.isPresent() && device.get().isActive()) {
|
||||
preKey.setRegistrationId(device.get().getRegistrationId());
|
||||
preKey.setIdentityKey(destination.getIdentityKey());
|
||||
filteredKeys.add(preKey);
|
||||
}
|
||||
public TargetKeys(Account destination, Optional<List<KeyRecord>> keys) {
|
||||
this.destination = destination;
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
if (filteredKeys.isEmpty()) return Optional.absent();
|
||||
else return Optional.of(new UnstructuredPreKeyList(filteredKeys));
|
||||
public Optional<List<KeyRecord>> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
public Account getDestination() {
|
||||
return destination;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
public interface PreKeyBase {
|
||||
|
||||
public long getKeyId();
|
||||
public String getPublicKey();
|
||||
|
||||
}
|
||||
@@ -3,16 +3,16 @@ package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class PreKeyStatus {
|
||||
public class PreKeyCount {
|
||||
|
||||
@JsonProperty
|
||||
private int count;
|
||||
|
||||
public PreKeyStatus(int count) {
|
||||
public PreKeyCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public PreKeyStatus() {}
|
||||
public PreKeyCount() {}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ 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;
|
||||
@@ -26,36 +25,36 @@ import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class UnstructuredPreKeyList {
|
||||
public class PreKeyResponseV1 {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private List<PreKey> keys;
|
||||
private List<PreKeyV1> keys;
|
||||
|
||||
@VisibleForTesting
|
||||
public UnstructuredPreKeyList() {}
|
||||
public PreKeyResponseV1() {}
|
||||
|
||||
public UnstructuredPreKeyList(PreKey preKey) {
|
||||
this.keys = new LinkedList<PreKey>();
|
||||
public PreKeyResponseV1(PreKeyV1 preKey) {
|
||||
this.keys = new LinkedList<>();
|
||||
this.keys.add(preKey);
|
||||
}
|
||||
|
||||
public UnstructuredPreKeyList(List<PreKey> preKeys) {
|
||||
public PreKeyResponseV1(List<PreKeyV1> preKeys) {
|
||||
this.keys = preKeys;
|
||||
}
|
||||
|
||||
public List<PreKey> getKeys() {
|
||||
public List<PreKeyV1> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof UnstructuredPreKeyList) ||
|
||||
((UnstructuredPreKeyList) o).keys.size() != keys.size())
|
||||
if (!(o instanceof PreKeyResponseV1) ||
|
||||
((PreKeyResponseV1) o).keys.size() != keys.size())
|
||||
return false;
|
||||
Iterator<PreKey> otherKeys = ((UnstructuredPreKeyList) o).keys.iterator();
|
||||
for (PreKey key : keys) {
|
||||
Iterator<PreKeyV1> otherKeys = ((PreKeyResponseV1) o).keys.iterator();
|
||||
for (PreKeyV1 key : keys) {
|
||||
if (!otherKeys.next().equals(key))
|
||||
return false;
|
||||
}
|
||||
@@ -64,7 +63,7 @@ public class UnstructuredPreKeyList {
|
||||
|
||||
public int hashCode() {
|
||||
int ret = 0xFBA4C795 * keys.size();
|
||||
for (PreKey key : keys)
|
||||
for (PreKeyV1 key : keys)
|
||||
ret ^= key.getPublicKey().hashCode();
|
||||
return ret;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
* 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.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 PreKeyList {
|
||||
public class PreKeyStateV1 {
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private PreKey lastResortKey;
|
||||
private PreKeyV1 lastResortKey;
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
@Valid
|
||||
private List<PreKey> keys;
|
||||
private List<PreKeyV1> keys;
|
||||
|
||||
public List<PreKey> getKeys() {
|
||||
public List<PreKeyV1> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setKeys(List<PreKey> keys) {
|
||||
public void setKeys(List<PreKeyV1> keys) {
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
public PreKey getLastResortKey() {
|
||||
public PreKeyV1 getLastResortKey() {
|
||||
return lastResortKey;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public void setLastResortKey(PreKey lastResortKey) {
|
||||
public void setLastResortKey(PreKeyV1 lastResortKey) {
|
||||
this.lastResortKey = lastResortKey;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -17,23 +17,14 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
import java.io.Serializable;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
|
||||
public class PreKey {
|
||||
|
||||
@JsonIgnore
|
||||
private long id;
|
||||
|
||||
@JsonIgnore
|
||||
private String number;
|
||||
public class PreKeyV1 implements PreKeyBase {
|
||||
|
||||
@JsonProperty
|
||||
private long deviceId;
|
||||
@@ -50,89 +41,43 @@ public class PreKey {
|
||||
@NotNull
|
||||
private String identityKey;
|
||||
|
||||
@JsonProperty
|
||||
private boolean lastResort;
|
||||
|
||||
@JsonProperty
|
||||
private int registrationId;
|
||||
|
||||
public PreKey() {}
|
||||
public PreKeyV1() {}
|
||||
|
||||
public PreKey(long id, String number, long deviceId, long keyId,
|
||||
String publicKey, boolean lastResort)
|
||||
public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey, int registrationId)
|
||||
{
|
||||
this.id = id;
|
||||
this.number = number;
|
||||
this.deviceId = deviceId;
|
||||
this.keyId = keyId;
|
||||
this.publicKey = publicKey;
|
||||
this.lastResort = lastResort;
|
||||
this.deviceId = deviceId;
|
||||
this.keyId = keyId;
|
||||
this.publicKey = publicKey;
|
||||
this.identityKey = identityKey;
|
||||
this.registrationId = registrationId;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public PreKey(long id, String number, long deviceId, long keyId,
|
||||
String publicKey, String identityKey, boolean lastResort)
|
||||
public PreKeyV1(long deviceId, long keyId, String publicKey, String identityKey)
|
||||
{
|
||||
this.id = id;
|
||||
this.number = number;
|
||||
this.deviceId = deviceId;
|
||||
this.keyId = keyId;
|
||||
this.publicKey = publicKey;
|
||||
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() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public void setPublicKey(String publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getKeyId() {
|
||||
return keyId;
|
||||
}
|
||||
|
||||
public void setKeyId(long keyId) {
|
||||
this.keyId = keyId;
|
||||
}
|
||||
|
||||
public String getIdentityKey() {
|
||||
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) {
|
||||
this.deviceId = deviceId;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -36,7 +36,8 @@ import org.whispersystems.textsecuregcm.entities.AttachmentUri;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContact;
|
||||
import org.whispersystems.textsecuregcm.entities.ClientContacts;
|
||||
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 javax.net.ssl.SSLContext;
|
||||
@@ -62,11 +63,12 @@ public class FederatedClient {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(FederatedClient.class);
|
||||
|
||||
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 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 ATTACHMENT_URI_PATH = "/v1/federation/attachment/%d";
|
||||
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 RELAY_MESSAGE_PATH = "/v1/federation/messages/%s/%d/%s";
|
||||
private static final String PREKEY_PATH_DEVICE_V1 = "/v1/federation/key/%s/%s";
|
||||
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 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 {
|
||||
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)
|
||||
.header("Authorization", authorizationHeader)
|
||||
@@ -119,7 +121,7 @@ public class FederatedClient {
|
||||
throw new WebApplicationException(clientResponseToResponse(response));
|
||||
}
|
||||
|
||||
return Optional.of(response.getEntity(UnstructuredPreKeyList.class));
|
||||
return Optional.of(response.getEntity(PreKeyResponseV1.class));
|
||||
|
||||
} catch (UniformInterfaceException | ClientHandlerException 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() {
|
||||
try {
|
||||
WebResource resource = client.resource(peer.getUrl()).path(USER_COUNT_PATH);
|
||||
|
||||
@@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
|
||||
|
||||
@Override
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.util.List;
|
||||
|
||||
public class Account implements Serializable {
|
||||
|
||||
public static final int MEMCACHE_VERION = 3;
|
||||
public static final int MEMCACHE_VERION = 4;
|
||||
|
||||
@JsonIgnore
|
||||
private long id;
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
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 java.io.Serializable;
|
||||
@@ -51,11 +53,15 @@ public class Device implements Serializable {
|
||||
@JsonProperty
|
||||
private int registrationId;
|
||||
|
||||
@JsonProperty
|
||||
private DeviceKey deviceKey;
|
||||
|
||||
public Device() {}
|
||||
|
||||
public Device(long id, String authToken, String salt,
|
||||
String signalingKey, String gcmId, String apnId,
|
||||
boolean fetchesMessages, int registrationId)
|
||||
boolean fetchesMessages, int registrationId,
|
||||
DeviceKey deviceKey)
|
||||
{
|
||||
this.id = id;
|
||||
this.authToken = authToken;
|
||||
@@ -65,6 +71,7 @@ public class Device implements Serializable {
|
||||
this.apnId = apnId;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.deviceKey = deviceKey;
|
||||
}
|
||||
|
||||
public String getApnId() {
|
||||
@@ -131,4 +138,12 @@ public class Device implements Serializable {
|
||||
public void setRegistrationId(int registrationId) {
|
||||
this.registrationId = registrationId;
|
||||
}
|
||||
|
||||
public DeviceKey getDeviceKey() {
|
||||
return deviceKey;
|
||||
}
|
||||
|
||||
public void setDeviceKey(DeviceKey deviceKey) {
|
||||
this.deviceKey = deviceKey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,9 @@ 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.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.UnstructuredPreKeyList;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyBase;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyV1;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyV2;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.ElementType;
|
||||
@@ -40,6 +41,7 @@ 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 {
|
||||
@@ -51,65 +53,64 @@ public abstract class Keys {
|
||||
abstract void removeKey(@Bind("id") long id);
|
||||
|
||||
@SqlBatch("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 List<PreKey> 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);
|
||||
"(:number, :device_id, :key_id, :public_key, :last_resort)")
|
||||
abstract void append(@PreKeyBinder List<KeyRecord> preKeys);
|
||||
|
||||
@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, @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")
|
||||
@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")
|
||||
public abstract int getCount(@Bind("number") String number, @Bind("device_id") long deviceId);
|
||||
|
||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||
public void store(String number, long deviceId, List<PreKey> keys, PreKey lastResortKey) {
|
||||
for (PreKey key : keys) {
|
||||
key.setNumber(number);
|
||||
key.setDeviceId(deviceId);
|
||||
public void store(String number, long deviceId, List<? extends PreKeyBase> keys, PreKeyBase lastResortKey) {
|
||||
List<KeyRecord> records = new LinkedList<>();
|
||||
|
||||
for (PreKeyBase key : keys) {
|
||||
records.add(new KeyRecord(0, number, deviceId, key.getKeyId(), key.getPublicKey(), false));
|
||||
}
|
||||
|
||||
lastResortKey.setNumber(number);
|
||||
lastResortKey.setDeviceId(deviceId);
|
||||
lastResortKey.setLastResort(true);
|
||||
records.add(new KeyRecord(0, number, deviceId, lastResortKey.getKeyId(),
|
||||
lastResortKey.getPublicKey(), true));
|
||||
|
||||
removeKeys(number, deviceId);
|
||||
append(keys);
|
||||
append(lastResortKey);
|
||||
append(records);
|
||||
}
|
||||
|
||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||
public Optional<UnstructuredPreKeyList> get(String number, long deviceId) {
|
||||
PreKey preKey = retrieveFirst(number, deviceId);
|
||||
public Optional<List<KeyRecord>> get(String number, long deviceId) {
|
||||
final KeyRecord record = retrieveFirst(number, deviceId);
|
||||
|
||||
if (preKey != null && !preKey.isLastResort()) {
|
||||
removeKey(preKey.getId());
|
||||
if (record != null && !record.isLastResort()) {
|
||||
removeKey(record.getId());
|
||||
} else if (record == null) {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
if (preKey != null) return Optional.of(new UnstructuredPreKeyList(preKey));
|
||||
else return Optional.absent();
|
||||
List<KeyRecord> results = new LinkedList<>();
|
||||
results.add(record);
|
||||
|
||||
return Optional.of(results);
|
||||
}
|
||||
|
||||
@Transaction(TransactionIsolationLevel.SERIALIZABLE)
|
||||
public Optional<UnstructuredPreKeyList> get(String number) {
|
||||
List<PreKey> preKeys = retrieveFirst(number);
|
||||
public Optional<List<KeyRecord>> get(String number) {
|
||||
List<KeyRecord> preKeys = retrieveFirst(number);
|
||||
|
||||
if (preKeys != null) {
|
||||
for (PreKey preKey : preKeys) {
|
||||
for (KeyRecord preKey : preKeys) {
|
||||
if (!preKey.isLastResort()) {
|
||||
removeKey(preKey.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (preKeys != null) return Optional.of(new UnstructuredPreKeyList(preKeys));
|
||||
if (preKeys != null) return Optional.of(preKeys);
|
||||
else return Optional.absent();
|
||||
}
|
||||
|
||||
@@ -120,16 +121,16 @@ public abstract class Keys {
|
||||
public static class PreKeyBinderFactory implements BinderFactory {
|
||||
@Override
|
||||
public Binder build(Annotation annotation) {
|
||||
return new Binder<PreKeyBinder, PreKey>() {
|
||||
return new Binder<PreKeyBinder, KeyRecord>() {
|
||||
@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("number", preKey.getNumber());
|
||||
sql.bind("device_id", preKey.getDeviceId());
|
||||
sql.bind("key_id", preKey.getKeyId());
|
||||
sql.bind("public_key", preKey.getPublicKey());
|
||||
sql.bind("last_resort", preKey.isLastResort() ? 1 : 0);
|
||||
sql.bind("id", record.getId());
|
||||
sql.bind("number", record.getNumber());
|
||||
sql.bind("device_id", record.getDeviceId());
|
||||
sql.bind("key_id", record.getKeyId());
|
||||
sql.bind("public_key", record.getPublicKey());
|
||||
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
|
||||
public PreKey map(int i, ResultSet resultSet, StatementContext statementContext)
|
||||
public KeyRecord map(int i, ResultSet resultSet, StatementContext statementContext)
|
||||
throws SQLException
|
||||
{
|
||||
return new PreKey(resultSet.getLong("id"), resultSet.getString("number"), resultSet.getLong("device_id"),
|
||||
resultSet.getLong("key_id"), resultSet.getString("public_key"),
|
||||
resultSet.getInt("last_resort") == 1);
|
||||
return new KeyRecord(resultSet.getLong("id"), resultSet.getString("number"),
|
||||
resultSet.getLong("device_id"), resultSet.getLong("key_id"),
|
||||
resultSet.getString("public_key"), resultSet.getInt("last_resort") == 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user