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

View File

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

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

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

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

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

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

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.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);

View File

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

View File

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

View File

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

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