From 06f80c320db1afa54a7593ce6826f3b509f09c81 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Tue, 1 Jul 2014 15:06:48 -0700 Subject: [PATCH] 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. --- .../textsecuregcm/WhisperServerService.java | 11 +- .../controllers/FederationController.java | 65 ++-- .../controllers/KeysController.java | 131 ++------ .../controllers/KeysControllerV1.java | 136 ++++++++ .../controllers/KeysControllerV2.java | 147 +++++++++ .../textsecuregcm/entities/DeviceKey.java | 46 +++ .../textsecuregcm/entities/PreKeyBase.java | 8 + .../{PreKeyStatus.java => PreKeyCount.java} | 6 +- .../entities/PreKeyResponseItemV2.java | 64 ++++ ...dPreKeyList.java => PreKeyResponseV1.java} | 25 +- .../entities/PreKeyResponseV2.java | 48 +++ .../{PreKeyList.java => PreKeyStateV1.java} | 17 +- .../textsecuregcm/entities/PreKeyStateV2.java | 74 +++++ .../entities/{PreKey.java => PreKeyV1.java} | 79 +---- .../textsecuregcm/entities/PreKeyV2.java | 82 +++++ .../federation/FederatedClient.java | 41 ++- .../federation/NonLimitedAccount.java | 2 +- .../textsecuregcm/storage/Account.java | 2 +- .../textsecuregcm/storage/Device.java | 17 +- .../textsecuregcm/storage/KeyRecord.java | 46 +++ .../textsecuregcm/storage/Keys.java | 85 ++--- .../controllers/FederatedControllerTest.java | 8 +- .../tests/controllers/KeyControllerTest.java | 293 ++++++++++++++---- .../controllers/MessageControllerTest.java | 7 +- .../tests/entities/PreKeyTest.java | 18 +- src/test/resources/fixtures/prekey_v2.json | 4 + 26 files changed, 1116 insertions(+), 346 deletions(-) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/controllers/KeysControllerV1.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/controllers/KeysControllerV2.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/entities/DeviceKey.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyBase.java rename src/main/java/org/whispersystems/textsecuregcm/entities/{PreKeyStatus.java => PreKeyCount.java} (71%) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseItemV2.java rename src/main/java/org/whispersystems/textsecuregcm/entities/{UnstructuredPreKeyList.java => PreKeyResponseV1.java} (71%) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseV2.java rename src/main/java/org/whispersystems/textsecuregcm/entities/{PreKeyList.java => PreKeyStateV1.java} (77%) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyStateV2.java rename src/main/java/org/whispersystems/textsecuregcm/entities/{PreKey.java => PreKeyV1.java} (55%) create mode 100644 src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyV2.java create mode 100644 src/main/java/org/whispersystems/textsecuregcm/storage/KeyRecord.java create mode 100644 src/test/resources/fixtures/prekey_v2.json diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 5e6a828ae..951be7e3b 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -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(new FederatedPeerAuthenticator(config.getFederationConfiguration()), @@ -158,9 +160,10 @@ public class WhisperServerService extends Application getKey(@Auth FederatedPeer peer, + @PathParam("number") String number) throws IOException { try { - return keysController.get(new NonLimitedAccount("Unknown", -1, peer.getName()), number, Optional.absent()); + return keysControllerV1.get(new NonLimitedAccount("Unknown", -1, peer.getName()), + number, Optional.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 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.absent()); + return keysControllerV1.getDeviceKey(new NonLimitedAccount("Unknown", -1, peer.getName()), + number, device, Optional.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 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.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) diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java index ade3e4e3f..8b439857f 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysController.java @@ -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 relay) - throws RateLimitExceededException + protected TargetKeys getLocalKeys(String number, String deviceIdSelector) + throws NoSuchUserException { - try { - if (account.isRateLimited()) { - rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId); - } - - Optional 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 relay) - throws RateLimitExceededException - { - UnstructuredPreKeyList results = getDeviceKey(account, number, String.valueOf(Device.MASTER_ID), relay); - return results.getKeys().get(0); - } - - private Optional getLocalKeys(String number, String deviceIdSelector) { Optional 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 preKeys = keys.get(number); - return getActiveKeys(destination.get(), preKeys); + Optional> preKeys = keys.get(number); + return new TargetKeys(destination.get(), preKeys); } long deviceId = Long.parseLong(deviceIdSelector); Optional targetDevice = destination.get().getDevice(deviceId); if (!targetDevice.isPresent() || !targetDevice.get().isActive()) { - return Optional.absent(); + throw new NoSuchUserException("Target device is inactive."); } - Optional preKeys = keys.get(number, deviceId); - return getActiveKeys(destination.get(), preKeys); + Optional> preKeys = keys.get(number, deviceId); + return new TargetKeys(destination.get(), preKeys); } catch (NumberFormatException e) { throw new WebApplicationException(Response.status(422).build()); } } - private Optional getActiveKeys(Account destination, - Optional preKeys) - { - if (!preKeys.isPresent()) return Optional.absent(); - List filteredKeys = new LinkedList<>(); + public static class TargetKeys { + private final Account destination; + private final Optional> keys; - for (PreKey preKey : preKeys.get().getKeys()) { - Optional 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> keys) { + this.destination = destination; + this.keys = keys; } - if (filteredKeys.isEmpty()) return Optional.absent(); - else return Optional.of(new UnstructuredPreKeyList(filteredKeys)); + public Optional> getKeys() { + return keys; + } + + public Account getDestination() { + return destination; + } } + } diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysControllerV1.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysControllerV1.java new file mode 100644 index 000000000..2b6d2cf53 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysControllerV1.java @@ -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 . + */ +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 getDeviceKey(@Auth Account account, + @PathParam("number") String number, + @PathParam("device_id") String deviceId, + @QueryParam("relay") Optional 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 preKeys = new LinkedList<>(); + Account destination = targetKeys.getDestination(); + + for (KeyRecord record : targetKeys.getKeys().get()) { + Optional 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 get(@Auth Account account, + @PathParam("number") String number, + @QueryParam("relay") Optional relay) + throws RateLimitExceededException + { + Optional 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(); + } + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysControllerV2.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysControllerV2.java new file mode 100644 index 000000000..bd7d0bc3d --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/KeysControllerV2.java @@ -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 . + */ +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 getDeviceKey(@Auth Account account, + @PathParam("number") String number, + @PathParam("device_id") String deviceId, + @QueryParam("relay") Optional 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 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()); + } + } + + + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceKey.java b/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceKey.java new file mode 100644 index 000000000..5cb09de85 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceKey.java @@ -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(); + } + } + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyBase.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyBase.java new file mode 100644 index 000000000..cf83943c5 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyBase.java @@ -0,0 +1,8 @@ +package org.whispersystems.textsecuregcm.entities; + +public interface PreKeyBase { + + public long getKeyId(); + public String getPublicKey(); + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyStatus.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyCount.java similarity index 71% rename from src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyStatus.java rename to src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyCount.java index 0da6d20a4..abbe51ff7 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyStatus.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyCount.java @@ -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; diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseItemV2.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseItemV2.java new file mode 100644 index 000000000..ee3e6b5b9 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseItemV2.java @@ -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 . + */ +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; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/UnstructuredPreKeyList.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseV1.java similarity index 71% rename from src/main/java/org/whispersystems/textsecuregcm/entities/UnstructuredPreKeyList.java rename to src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseV1.java index 5ce87276a..b5fe49ecf 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/UnstructuredPreKeyList.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseV1.java @@ -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 keys; + private List keys; @VisibleForTesting - public UnstructuredPreKeyList() {} + public PreKeyResponseV1() {} - public UnstructuredPreKeyList(PreKey preKey) { - this.keys = new LinkedList(); + public PreKeyResponseV1(PreKeyV1 preKey) { + this.keys = new LinkedList<>(); this.keys.add(preKey); } - public UnstructuredPreKeyList(List preKeys) { + public PreKeyResponseV1(List preKeys) { this.keys = preKeys; } - public List getKeys() { + public List 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 otherKeys = ((UnstructuredPreKeyList) o).keys.iterator(); - for (PreKey key : keys) { + Iterator 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; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseV2.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseV2.java new file mode 100644 index 000000000..ac11fe345 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyResponseV2.java @@ -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 . + */ +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 devices; + + public PreKeyResponseV2() {} + + public PreKeyResponseV2(String identityKey, List devices) { + this.identityKey = identityKey; + this.devices = devices; + } + + @VisibleForTesting + public String getIdentityKey() { + return identityKey; + } + + @VisibleForTesting + public List getDevices() { + return devices; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyList.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyStateV1.java similarity index 77% rename from src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyList.java rename to src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyStateV1.java index df2c1d7f5..7c789eee2 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyList.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyStateV1.java @@ -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 keys; + private List keys; - public List getKeys() { + public List getKeys() { return keys; } @VisibleForTesting - public void setKeys(List keys) { + public void setKeys(List 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; } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyStateV2.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyStateV2.java new file mode 100644 index 000000000..25ffd4e3c --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyStateV2.java @@ -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 . + */ +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 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 keys, PreKeyV2 lastResortKey) { + this.identityKey = identityKey; + this.deviceKey = deviceKey; + this.preKeys = keys; + this.lastResortKey = lastResortKey; + } + + public List getPreKeys() { + return preKeys; + } + + public DeviceKey getDeviceKey() { + return deviceKey; + } + + public String getIdentityKey() { + return identityKey; + } + + public PreKeyV2 getLastResortKey() { + return lastResortKey; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyV1.java similarity index 55% rename from src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java rename to src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyV1.java index 777e23ff9..deeb9d95e 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKey.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyV1.java @@ -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; } diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyV2.java b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyV2.java new file mode 100644 index 000000000..59de13e80 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeyV2.java @@ -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 . + */ + +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(); + } + } + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java b/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java index 8168285e8..97ac15f0d 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java +++ b/src/main/java/org/whispersystems/textsecuregcm/federation/FederatedClient.java @@ -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 getKeys(String destination, String device) { + public Optional 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 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); diff --git a/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java b/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java index 6b3090adf..2c9b5f817 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java +++ b/src/main/java/org/whispersystems/textsecuregcm/federation/NonLimitedAccount.java @@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account { @Override public Optional 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)); } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index 33a42c1a6..5d8f5c9c1 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -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; diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java index 54987d007..4478dd3dd 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java @@ -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; + } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/KeyRecord.java b/src/main/java/org/whispersystems/textsecuregcm/storage/KeyRecord.java new file mode 100644 index 000000000..6f5d0bc47 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/KeyRecord.java @@ -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; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java b/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java index 361d750fe..e226c34ae 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java +++ b/src/main/java/org/whispersystems/textsecuregcm/storage/Keys.java @@ -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 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 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 retrieveFirst(@Bind("number") String number); + abstract List 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 keys, PreKey lastResortKey) { - for (PreKey key : keys) { - key.setNumber(number); - key.setDeviceId(deviceId); + public void store(String number, long deviceId, List keys, PreKeyBase lastResortKey) { + List 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 get(String number, long deviceId) { - PreKey preKey = retrieveFirst(number, deviceId); + public Optional> 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 results = new LinkedList<>(); + results.add(record); + + return Optional.of(results); } @Transaction(TransactionIsolationLevel.SERIALIZABLE) - public Optional get(String number) { - List preKeys = retrieveFirst(number); + public Optional> get(String number) { + List 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() { + return new Binder() { @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 { + public static class PreKeyMapper implements ResultSetMapper { @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); } } diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java index d04674a06..b43d7a7d9 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/FederatedControllerTest.java @@ -52,7 +52,7 @@ public class FederatedControllerTest { public final ResourceTestRule resources = ResourceTestRule.builder() .addProvider(AuthHelper.getAuthenticator()) .addResource(new FederationController(accountsManager, - null, null, + null, null, null, messageController)) .build(); @@ -61,12 +61,12 @@ public class FederatedControllerTest { @Before public void setup() throws Exception { List singleDeviceList = new LinkedList() {{ - add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111)); + add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null)); }}; List multiDeviceList = new LinkedList() {{ - add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222)); - add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333)); + add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null)); + add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null)); }}; Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java index e6edabd97..412585980 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/KeyControllerTest.java @@ -6,18 +6,22 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.whispersystems.textsecuregcm.controllers.KeysController; -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.controllers.KeysControllerV1; +import org.whispersystems.textsecuregcm.controllers.KeysControllerV2; +import org.whispersystems.textsecuregcm.entities.DeviceKey; +import org.whispersystems.textsecuregcm.entities.PreKeyCount; +import org.whispersystems.textsecuregcm.entities.PreKeyResponseV1; +import org.whispersystems.textsecuregcm.entities.PreKeyResponseV2; +import org.whispersystems.textsecuregcm.entities.PreKeyStateV1; +import org.whispersystems.textsecuregcm.entities.PreKeyStateV2; +import org.whispersystems.textsecuregcm.entities.PreKeyV1; +import org.whispersystems.textsecuregcm.entities.PreKeyV2; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.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 org.whispersystems.textsecuregcm.tests.util.AuthHelper; @@ -36,10 +40,18 @@ public class KeyControllerTest { private static int SAMPLE_REGISTRATION_ID = 999; private static int SAMPLE_REGISTRATION_ID2 = 1002; + private static int SAMPLE_REGISTRATION_ID4 = 1555; + + private final KeyRecord SAMPLE_KEY = new KeyRecord(1, EXISTS_NUMBER, Device.MASTER_ID, 1234, "test1", false); + private final KeyRecord SAMPLE_KEY2 = new KeyRecord(2, EXISTS_NUMBER, 2, 5667, "test3", false ); + private final KeyRecord SAMPLE_KEY3 = new KeyRecord(3, EXISTS_NUMBER, 3, 334, "test5", false ); + private final KeyRecord SAMPLE_KEY4 = new KeyRecord(4, EXISTS_NUMBER, 4, 336, "test6", false ); + + + private final DeviceKey SAMPLE_DEVICE_KEY = new DeviceKey(1111, "foofoo", "sig11"); + private final DeviceKey SAMPLE_DEVICE_KEY2 = new DeviceKey(2222, "foobar", "sig22"); + private final DeviceKey SAMPLE_DEVICE_KEY3 = new DeviceKey(3333, "barfoo", "sig33"); - private final PreKey SAMPLE_KEY = new PreKey(1, EXISTS_NUMBER, Device.MASTER_ID, 1234, "test1", "test2", false); - private final PreKey SAMPLE_KEY2 = new PreKey(2, EXISTS_NUMBER, 2, 5667, "test3", "test4,", false ); - private final PreKey SAMPLE_KEY3 = new PreKey(3, EXISTS_NUMBER, 3, 334, "test5", "test6", false ); private final Keys keys = mock(Keys.class ); private final AccountsManager accounts = mock(AccountsManager.class); private final Account existsAccount = mock(Account.class ); @@ -50,25 +62,47 @@ public class KeyControllerTest { @Rule public final ResourceTestRule resources = ResourceTestRule.builder() .addProvider(AuthHelper.getAuthenticator()) - .addResource(new KeysController(rateLimiters, keys, accounts, null)) + .addResource(new KeysControllerV1(rateLimiters, keys, accounts, null)) + .addResource(new KeysControllerV2(rateLimiters, keys, accounts, null)) .build(); @Before public void setup() { - Device sampleDevice = mock(Device.class ); - Device sampleDevice2 = mock(Device.class); - Device sampleDevice3 = mock(Device.class); + final Device sampleDevice = mock(Device.class ); + final Device sampleDevice2 = mock(Device.class); + final Device sampleDevice3 = mock(Device.class); + final Device sampleDevice4 = mock(Device.class); + + List allDevices = new LinkedList() {{ + add(sampleDevice); + add(sampleDevice2); + add(sampleDevice3); + add(sampleDevice4); + }}; when(sampleDevice.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID); when(sampleDevice2.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2); when(sampleDevice3.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID2); + when(sampleDevice4.getRegistrationId()).thenReturn(SAMPLE_REGISTRATION_ID4); when(sampleDevice.isActive()).thenReturn(true); when(sampleDevice2.isActive()).thenReturn(true); when(sampleDevice3.isActive()).thenReturn(false); + when(sampleDevice4.isActive()).thenReturn(true); + when(sampleDevice.getDeviceKey()).thenReturn(SAMPLE_DEVICE_KEY); + when(sampleDevice2.getDeviceKey()).thenReturn(SAMPLE_DEVICE_KEY2); + when(sampleDevice3.getDeviceKey()).thenReturn(SAMPLE_DEVICE_KEY3); + when(sampleDevice4.getDeviceKey()).thenReturn(null); + when(sampleDevice.getId()).thenReturn(1L); + when(sampleDevice2.getId()).thenReturn(2L); + when(sampleDevice3.getId()).thenReturn(3L); + when(sampleDevice4.getId()).thenReturn(4L); when(existsAccount.getDevice(1L)).thenReturn(Optional.of(sampleDevice)); when(existsAccount.getDevice(2L)).thenReturn(Optional.of(sampleDevice2)); when(existsAccount.getDevice(3L)).thenReturn(Optional.of(sampleDevice3)); + when(existsAccount.getDevice(4L)).thenReturn(Optional.of(sampleDevice4)); + when(existsAccount.getDevice(22L)).thenReturn(Optional.absent()); + when(existsAccount.getDevices()).thenReturn(allDevices); when(existsAccount.isActive()).thenReturn(true); when(existsAccount.getIdentityKey()).thenReturn("existsidentitykey"); @@ -77,86 +111,155 @@ public class KeyControllerTest { when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter); - when(keys.get(eq(EXISTS_NUMBER), eq(1L))).thenAnswer(new Answer>() { - @Override - public Optional answer(InvocationOnMock invocationOnMock) throws Throwable { - return Optional.of(new UnstructuredPreKeyList(cloneKey(SAMPLE_KEY))); - } - }); + List singleDevice = new LinkedList<>(); + singleDevice.add(SAMPLE_KEY); + when(keys.get(eq(EXISTS_NUMBER), eq(1L))).thenReturn(Optional.of(singleDevice)); - when(keys.get(eq(NOT_EXISTS_NUMBER), eq(1L))).thenReturn(Optional.absent()); + when(keys.get(eq(NOT_EXISTS_NUMBER), eq(1L))).thenReturn(Optional.>absent()); - when(keys.get(EXISTS_NUMBER)).thenAnswer(new Answer>() { - @Override - public Optional answer(InvocationOnMock invocationOnMock) throws Throwable { - List allKeys = new LinkedList<>(); - allKeys.add(cloneKey(SAMPLE_KEY)); - allKeys.add(cloneKey(SAMPLE_KEY2)); - allKeys.add(cloneKey(SAMPLE_KEY3)); - - return Optional.of(new UnstructuredPreKeyList(allKeys)); - } - }); + List multiDevice = new LinkedList<>(); + multiDevice.add(SAMPLE_KEY); + multiDevice.add(SAMPLE_KEY2); + multiDevice.add(SAMPLE_KEY3); + multiDevice.add(SAMPLE_KEY4); + when(keys.get(EXISTS_NUMBER)).thenReturn(Optional.of(multiDevice)); when(keys.getCount(eq(AuthHelper.VALID_NUMBER), eq(1L))).thenReturn(5); + + when(AuthHelper.VALID_DEVICE.getDeviceKey()).thenReturn(new DeviceKey(89898, "zoofarb", "sigvalid")); when(AuthHelper.VALID_ACCOUNT.getIdentityKey()).thenReturn(null); } @Test - public void validKeyStatusTest() throws Exception { - PreKeyStatus result = resources.client().resource("/v1/keys") + public void validKeyStatusTestV1() throws Exception { + PreKeyCount result = resources.client().resource("/v1/keys") .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) - .get(PreKeyStatus.class); + .get(PreKeyCount.class); assertThat(result.getCount() == 4); verify(keys).getCount(eq(AuthHelper.VALID_NUMBER), eq(1L)); } + @Test + public void validKeyStatusTestV2() throws Exception { + PreKeyCount result = resources.client().resource("/v2/keys") + .header("Authorization", + AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(PreKeyCount.class); + + assertThat(result.getCount() == 4); + + verify(keys).getCount(eq(AuthHelper.VALID_NUMBER), eq(1L)); + } + + @Test public void validLegacyRequestTest() throws Exception { - PreKey result = resources.client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER)) + PreKeyV1 result = resources.client().resource(String.format("/v1/keys/%s", EXISTS_NUMBER)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) - .get(PreKey.class); + .get(PreKeyV1.class); assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId()); assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey()); assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey()); - assertThat(result.getId() == 0); - assertThat(result.getNumber() == null); - verify(keys).get(eq(EXISTS_NUMBER), eq(1L)); verifyNoMoreInteractions(keys); } @Test - public void validMultiRequestTest() throws Exception { - UnstructuredPreKeyList results = resources.client().resource(String.format("/v1/keys/%s/*", EXISTS_NUMBER)) + public void validSingleRequestTestV2() throws Exception { + PreKeyResponseV2 result = resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER)) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(PreKeyResponseV2.class); + + assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey()); + assertThat(result.getDevices().size()).isEqualTo(1); + assertThat(result.getDevices().get(0).getPreKey().getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId()); + assertThat(result.getDevices().get(0).getPreKey().getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey()); + assertThat(result.getDevices().get(0).getDeviceKey()).isEqualTo(existsAccount.getDevice(1).get().getDeviceKey()); + + verify(keys).get(eq(EXISTS_NUMBER), eq(1L)); + verifyNoMoreInteractions(keys); + } + + + @Test + public void validMultiRequestTestV1() throws Exception { + PreKeyResponseV1 results = resources.client().resource(String.format("/v1/keys/%s/*", EXISTS_NUMBER)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) - .get(UnstructuredPreKeyList.class); + .get(PreKeyResponseV1.class); - assertThat(results.getKeys().size()).isEqualTo(2); + assertThat(results.getKeys().size()).isEqualTo(3); - PreKey result = results.getKeys().get(0); + PreKeyV1 result = results.getKeys().get(0); assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId()); assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey()); assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey()); assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID); - assertThat(result.getId() == 0); - assertThat(result.getNumber() == null); - result = results.getKeys().get(1); assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId()); assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey()); assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey()); assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID2); - assertThat(result.getId() == 0); - assertThat(result.getNumber() == null); + result = results.getKeys().get(2); + assertThat(result.getKeyId()).isEqualTo(SAMPLE_KEY4.getKeyId()); + assertThat(result.getPublicKey()).isEqualTo(SAMPLE_KEY4.getPublicKey()); + assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey()); + assertThat(result.getRegistrationId()).isEqualTo(SAMPLE_REGISTRATION_ID4); + + verify(keys).get(eq(EXISTS_NUMBER)); + verifyNoMoreInteractions(keys); + } + + @Test + public void validMultiRequestTestV2() throws Exception { + PreKeyResponseV2 results = resources.client().resource(String.format("/v2/keys/%s/*", EXISTS_NUMBER)) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(PreKeyResponseV2.class); + + assertThat(results.getDevices().size()).isEqualTo(3); + assertThat(results.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey()); + + PreKeyV2 deviceKey = results.getDevices().get(0).getDeviceKey(); + PreKeyV2 preKey = results.getDevices().get(0).getPreKey(); + long registrationId = results.getDevices().get(0).getRegistrationId(); + long deviceId = results.getDevices().get(0).getDeviceId(); + + assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId()); + assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey()); + assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID); + assertThat(deviceKey.getKeyId()).isEqualTo(SAMPLE_DEVICE_KEY.getKeyId()); + assertThat(deviceKey.getPublicKey()).isEqualTo(SAMPLE_DEVICE_KEY.getPublicKey()); + assertThat(deviceId).isEqualTo(1); + + deviceKey = results.getDevices().get(1).getDeviceKey(); + preKey = results.getDevices().get(1).getPreKey(); + registrationId = results.getDevices().get(1).getRegistrationId(); + deviceId = results.getDevices().get(1).getDeviceId(); + + assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY2.getKeyId()); + assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY2.getPublicKey()); + assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID2); + assertThat(deviceKey.getKeyId()).isEqualTo(SAMPLE_DEVICE_KEY2.getKeyId()); + assertThat(deviceKey.getPublicKey()).isEqualTo(SAMPLE_DEVICE_KEY2.getPublicKey()); + assertThat(deviceId).isEqualTo(2); + + deviceKey = results.getDevices().get(2).getDeviceKey(); + preKey = results.getDevices().get(2).getPreKey(); + registrationId = results.getDevices().get(2).getRegistrationId(); + deviceId = results.getDevices().get(2).getDeviceId(); + + assertThat(preKey.getKeyId()).isEqualTo(SAMPLE_KEY4.getKeyId()); + assertThat(preKey.getPublicKey()).isEqualTo(SAMPLE_KEY4.getPublicKey()); + assertThat(registrationId).isEqualTo(SAMPLE_REGISTRATION_ID4); + assertThat(deviceKey).isNull(); + assertThat(deviceId).isEqualTo(4); verify(keys).get(eq(EXISTS_NUMBER)); verifyNoMoreInteractions(keys); @@ -164,7 +267,7 @@ public class KeyControllerTest { @Test - public void invalidRequestTest() throws Exception { + public void invalidRequestTestV1() throws Exception { ClientResponse response = resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) .get(ClientResponse.class); @@ -173,7 +276,25 @@ public class KeyControllerTest { } @Test - public void unauthorizedRequestTest() throws Exception { + public void invalidRequestTestV2() throws Exception { + ClientResponse response = resources.client().resource(String.format("/v2/keys/%s", NOT_EXISTS_NUMBER)) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(ClientResponse.class); + + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404); + } + + @Test + public void anotherInvalidRequestTestV2() throws Exception { + ClientResponse response = resources.client().resource(String.format("/v2/keys/%s/22", EXISTS_NUMBER)) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(ClientResponse.class); + + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(404); + } + + @Test + public void unauthorizedRequestTestV1() throws Exception { ClientResponse response = resources.client().resource(String.format("/v1/keys/%s", NOT_EXISTS_NUMBER)) .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD)) @@ -189,15 +310,31 @@ public class KeyControllerTest { } @Test - public void putKeysTest() throws Exception { - final PreKey newKey = new PreKey(0, null, 1L, 31337, "foobar", "foobarbaz", false); - final PreKey lastResortKey = new PreKey(0, null, 1L, 0xFFFFFF, "fooz", "foobarbaz", false); + public void unauthorizedRequestTestV2() throws Exception { + ClientResponse response = + resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER)) + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD)) + .get(ClientResponse.class); - List preKeys = new LinkedList() {{ + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401); + + response = + resources.client().resource(String.format("/v2/keys/%s/1", EXISTS_NUMBER)) + .get(ClientResponse.class); + + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(401); + } + + @Test + public void putKeysTestV1() throws Exception { + final PreKeyV1 newKey = new PreKeyV1(1L, 31337, "foobar", "foobarbaz"); + final PreKeyV1 lastResortKey = new PreKeyV1(1L, 0xFFFFFF, "fooz", "foobarbaz"); + + List preKeys = new LinkedList() {{ add(newKey); }}; - PreKeyList preKeyList = new PreKeyList(); + PreKeyStateV1 preKeyList = new PreKeyStateV1(); preKeyList.setKeys(preKeys); preKeyList.setLastResortKey(lastResortKey); @@ -209,11 +346,11 @@ public class KeyControllerTest { assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(204); - ArgumentCaptor listCaptor = ArgumentCaptor.forClass(List.class ); - ArgumentCaptor lastResortCaptor = ArgumentCaptor.forClass(PreKey.class); + ArgumentCaptor listCaptor = ArgumentCaptor.forClass(List.class ); + ArgumentCaptor lastResortCaptor = ArgumentCaptor.forClass(PreKeyV1.class); verify(keys).store(eq(AuthHelper.VALID_NUMBER), eq(1L), listCaptor.capture(), lastResortCaptor.capture()); - List capturedList = listCaptor.getValue(); + List capturedList = listCaptor.getValue(); assertThat(capturedList.size() == 1); assertThat(capturedList.get(0).getIdentityKey().equals("foobarbaz")); assertThat(capturedList.get(0).getKeyId() == 31337); @@ -226,9 +363,39 @@ public class KeyControllerTest { verify(accounts).update(AuthHelper.VALID_ACCOUNT); } - private PreKey cloneKey(PreKey source) { - return new PreKey(source.getId(), source.getNumber(), source.getDeviceId(), source.getKeyId(), - source.getPublicKey(), source.getIdentityKey(), source.isLastResort()); + @Test + public void putKeysTestV2() throws Exception { + final PreKeyV2 preKey = new PreKeyV2(31337, "foobar"); + final PreKeyV2 lastResortKey = new PreKeyV2(31339, "barbar"); + final DeviceKey deviceKey = new DeviceKey(31338, "foobaz", "myvalidsig"); + final String identityKey = "barbar"; + + List preKeys = new LinkedList() {{ + add(preKey); + }}; + + PreKeyStateV2 preKeyState = new PreKeyStateV2(identityKey, deviceKey, preKeys, lastResortKey); + + ClientResponse response = + resources.client().resource("/v2/keys") + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .type(MediaType.APPLICATION_JSON_TYPE) + .put(ClientResponse.class, preKeyState); + + assertThat(response.getClientResponseStatus().getStatusCode()).isEqualTo(204); + + ArgumentCaptor listCaptor = ArgumentCaptor.forClass(List.class); + verify(keys).store(eq(AuthHelper.VALID_NUMBER), eq(1L), listCaptor.capture(), eq(lastResortKey)); + + List capturedList = listCaptor.getValue(); + assertThat(capturedList.size() == 1); + assertThat(capturedList.get(0).getKeyId() == 31337); + assertThat(capturedList.get(0).getPublicKey().equals("foobar")); + + verify(AuthHelper.VALID_ACCOUNT).setIdentityKey(eq("barbar")); + verify(AuthHelper.VALID_DEVICE).setDeviceKey(eq(deviceKey)); + verify(accounts).update(AuthHelper.VALID_ACCOUNT); } + } \ No newline at end of file diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java index 97f434a50..bb41243b4 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/MessageControllerTest.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Optional; import com.sun.jersey.api.client.ClientResponse; import org.junit.Before; -import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.whispersystems.textsecuregcm.controllers.MessageController; @@ -59,12 +58,12 @@ public class MessageControllerTest { @Before public void setup() throws Exception { List singleDeviceList = new LinkedList() {{ - add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111)); + add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 111, null)); }}; List multiDeviceList = new LinkedList() {{ - add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222)); - add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333)); + add(new Device(1, "foo", "bar", "baz", "isgcm", null, false, 222, null)); + add(new Device(2, "foo", "bar", "baz", "isgcm", null, false, 333, null)); }}; Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, false, singleDeviceList); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java index f4bd4bdd0..b23698141 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/entities/PreKeyTest.java @@ -2,7 +2,8 @@ package org.whispersystems.textsecuregcm.tests.entities; import org.junit.Test; import org.whispersystems.textsecuregcm.entities.ClientContact; -import org.whispersystems.textsecuregcm.entities.PreKey; +import org.whispersystems.textsecuregcm.entities.PreKeyV1; +import org.whispersystems.textsecuregcm.entities.PreKeyV2; import org.whispersystems.textsecuregcm.util.Util; import static org.hamcrest.CoreMatchers.equalTo; @@ -13,8 +14,8 @@ import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.*; public class PreKeyTest { @Test - public void serializeToJSON() throws Exception { - PreKey preKey = new PreKey(1, "+14152222222", 1, 1234, "test", "identityTest", false); + public void serializeToJSONV1() throws Exception { + PreKeyV1 preKey = new PreKeyV1(1, 1234, "test", "identityTest"); preKey.setRegistrationId(987); assertThat("Basic Contact Serialization works", @@ -23,7 +24,7 @@ public class PreKeyTest { } @Test - public void deserializeFromJSON() throws Exception { + public void deserializeFromJSONV() throws Exception { ClientContact contact = new ClientContact(Util.getContactToken("+14152222222"), "whisper", true); @@ -32,4 +33,13 @@ public class PreKeyTest { is(contact)); } + @Test + public void serializeToJSONV2() throws Exception { + PreKeyV2 preKey = new PreKeyV2(1234, "test"); + + assertThat("PreKeyV2 Serialization works", + asJson(preKey), + is(equalTo(jsonFixture("fixtures/prekey_v2.json")))); + } + } diff --git a/src/test/resources/fixtures/prekey_v2.json b/src/test/resources/fixtures/prekey_v2.json new file mode 100644 index 000000000..1feafdaea --- /dev/null +++ b/src/test/resources/fixtures/prekey_v2.json @@ -0,0 +1,4 @@ +{ + "keyId" : 1234, + "publicKey" : "test" +} \ No newline at end of file