Secret sender

This commit is contained in:
Moxie Marlinspike 2018-04-24 10:21:41 -07:00
parent 8513b6fbd5
commit 7e026a7072
94 changed files with 4523 additions and 1717 deletions

View File

@ -102,12 +102,12 @@
<dependency>
<groupId>org.whispersystems</groupId>
<artifactId>websocket-resources</artifactId>
<version>0.5.4</version>
<version>0.5.8</version>
</dependency>
<dependency>
<groupId>org.whispersystems</groupId>
<artifactId>dropwizard-simpleauth</artifactId>
<version>0.3.0</version>
<artifactId>curve25519-java</artifactId>
<version>0.5.0</version>
</dependency>
<dependency>

View File

@ -21,22 +21,48 @@ option java_outer_classname = "MessageProtos";
message Envelope {
enum Type {
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
RECEIPT = 5;
UNKNOWN = 0;
CIPHERTEXT = 1;
KEY_EXCHANGE = 2;
PREKEY_BUNDLE = 3;
RECEIPT = 5;
UNIDENTIFIED_SENDER = 6;
}
optional Type type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage XXX -- Remove after 10/01/15
optional bytes content = 8; // Contains an encrypted Content
optional Type type = 1;
optional string source = 2;
optional uint32 sourceDevice = 7;
optional string relay = 3;
optional uint64 timestamp = 5;
optional bytes legacyMessage = 6; // Contains an encrypted DataMessage XXX -- Remove after 10/01/15
optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9;
optional uint64 server_timestamp = 10;
}
message ProvisioningUuid {
optional string uuid = 1;
}
}
message ServerCertificate {
message Certificate {
optional uint32 id = 1;
optional bytes key = 2;
}
optional bytes certificate = 1;
optional bytes signature = 2;
}
message SenderCertificate {
message Certificate {
optional string sender = 1;
optional uint32 senderDevice = 2;
optional fixed64 expires = 3;
optional bytes identityKey = 4;
optional ServerCertificate signer = 5;
}
optional bytes certificate = 1;
optional bytes signature = 2;
}

View File

@ -1,6 +1,5 @@
package org.whispersystems.dispatch;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.io.RedisPubSubConnectionFactory;
@ -9,10 +8,12 @@ import org.whispersystems.dispatch.redis.PubSubReply;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class DispatchManager extends Thread {
private final Logger logger = LoggerFactory.getLogger(DispatchManager.class);
@ -45,7 +46,7 @@ public class DispatchManager extends Thread {
}
public synchronized void subscribe(String name, DispatchChannel dispatchChannel) {
Optional<DispatchChannel> previous = Optional.fromNullable(subscriptions.get(name));
Optional<DispatchChannel> previous = Optional.ofNullable(subscriptions.get(name));
subscriptions.put(name, dispatchChannel);
try {
@ -60,7 +61,7 @@ public class DispatchManager extends Thread {
}
public synchronized void unsubscribe(String name, DispatchChannel channel) {
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(name));
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(name));
if (subscription.isPresent() && subscription.get() == channel) {
subscriptions.remove(name);
@ -105,7 +106,7 @@ public class DispatchManager extends Thread {
}
private void dispatchSubscribe(final PubSubReply reply) {
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(reply.getChannel()));
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(reply.getChannel()));
if (subscription.isPresent()) {
dispatchSubscription(reply.getChannel(), subscription.get());
@ -115,7 +116,7 @@ public class DispatchManager extends Thread {
}
private void dispatchMessage(PubSubReply reply) {
Optional<DispatchChannel> subscription = Optional.fromNullable(subscriptions.get(reply.getChannel()));
Optional<DispatchChannel> subscription = Optional.ofNullable(subscriptions.get(reply.getChannel()));
if (subscription.isPresent()) {
dispatchMessage(reply.getChannel(), subscription.get(), reply.getContent().get());

View File

@ -1,6 +1,5 @@
package org.whispersystems.dispatch.redis;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dispatch.io.RedisInputStream;
@ -14,6 +13,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
public class PubSubConnection {
@ -98,12 +98,12 @@ public class PubSubConnection {
private PubSubReply readUnsubscribeReply() throws IOException {
String channelName = readSubscriptionReply();
return new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, channelName, Optional.<byte[]>absent());
return new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, channelName, Optional.empty());
}
private PubSubReply readSubscribeReply() throws IOException {
String channelName = readSubscriptionReply();
return new PubSubReply(PubSubReply.Type.SUBSCRIBE, channelName, Optional.<byte[]>absent());
return new PubSubReply(PubSubReply.Type.SUBSCRIBE, channelName, Optional.empty());
}
private String readSubscriptionReply() throws IOException {

View File

@ -1,7 +1,9 @@
package org.whispersystems.dispatch.redis;
import com.google.common.base.Optional;
import java.util.Optional;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class PubSubReply {
public enum Type {
@ -11,7 +13,7 @@ public class PubSubReply {
}
private final Type type;
private final String channel;
private final String channel;
private final Optional<byte[]> content;
public PubSubReply(Type type, String channel, Optional<byte[]> content) {

View File

@ -17,20 +17,7 @@
package org.whispersystems.textsecuregcm;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.configuration.ApnConfiguration;
import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.configuration.GcmConfiguration;
import org.whispersystems.textsecuregcm.configuration.MaxDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.MessageCacheConfiguration;
import org.whispersystems.textsecuregcm.configuration.ProfilesConfiguration;
import org.whispersystems.textsecuregcm.configuration.PushConfiguration;
import org.whispersystems.textsecuregcm.configuration.RateLimitsConfiguration;
import org.whispersystems.textsecuregcm.configuration.RedisConfiguration;
import org.whispersystems.textsecuregcm.configuration.TestDeviceConfiguration;
import org.whispersystems.textsecuregcm.configuration.TurnConfiguration;
import org.whispersystems.textsecuregcm.configuration.TwilioConfiguration;
import org.whispersystems.textsecuregcm.configuration.*;
import org.whispersystems.websocket.configuration.WebSocketConfiguration;
import javax.validation.Valid;
@ -101,10 +88,6 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private List<MaxDeviceConfiguration> maxDevices = new LinkedList<>();
@Valid
@JsonProperty
private FederationConfiguration federation = new FederationConfiguration();
@Valid
@NotNull
@JsonProperty
@ -143,6 +126,11 @@ public class WhisperServerConfiguration extends Configuration {
@JsonProperty
private ApnConfiguration apn;
@Valid
@NotNull
@JsonProperty
private UnidentifiedDeliveryConfiguration unidentifiedDelivery;
public WebSocketConfiguration getWebSocketConfiguration() {
return webSocket;
}
@ -195,10 +183,6 @@ public class WhisperServerConfiguration extends Configuration {
return limits;
}
public FederationConfiguration getFederationConfiguration() {
return federation;
}
public TurnConfiguration getTurnConfiguration() {
return turn;
}
@ -215,6 +199,10 @@ public class WhisperServerConfiguration extends Configuration {
return profiles;
}
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {
return unidentifiedDelivery;
}
public Map<String, Integer> getTestDevices() {
Map<String, Integer> results = new HashMap<>();

View File

@ -20,31 +20,24 @@ import com.codahale.metrics.SharedMetricRegistries;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.google.common.base.Optional;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import org.skife.jdbi.v2.DBI;
import org.whispersystems.dispatch.DispatchManager;
import org.whispersystems.dropwizard.simpleauth.AuthDynamicFeature;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator;
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.controllers.DeviceController;
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
import org.whispersystems.textsecuregcm.controllers.KeepAliveController;
import org.whispersystems.textsecuregcm.controllers.KeysController;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.controllers.ProfileController;
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
@ -75,6 +68,7 @@ import org.whispersystems.textsecuregcm.websocket.AuthenticatedConnectListener;
import org.whispersystems.textsecuregcm.websocket.DeadLetterHandler;
import org.whispersystems.textsecuregcm.websocket.ProvisioningConnectListener;
import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.workers.CertificateCommand;
import org.whispersystems.textsecuregcm.workers.DeleteUserCommand;
import org.whispersystems.textsecuregcm.workers.DirectoryCommand;
import org.whispersystems.textsecuregcm.workers.PeriodicStatsCommand;
@ -88,9 +82,13 @@ import javax.servlet.FilterRegistration;
import javax.servlet.ServletRegistration;
import java.security.Security;
import java.util.EnumSet;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.Application;
import io.dropwizard.auth.AuthDynamicFeature;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.DBIFactory;
import io.dropwizard.setup.Bootstrap;
@ -109,6 +107,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
bootstrap.addCommand(new TrimMessagesCommand());
bootstrap.addCommand(new PeriodicStatsCommand());
bootstrap.addCommand(new DeleteUserCommand());
bootstrap.addCommand(new CertificateCommand());
bootstrap.addBundle(new NameableMigrationsBundle<WhisperServerConfiguration>("accountdb", "accountsdb.xml") {
@Override
public DataSourceFactory getDataSourceFactory(WhisperServerConfiguration configuration) {
@ -163,7 +162,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
PendingAccountsManager pendingAccountsManager = new PendingAccountsManager(pendingAccounts, cacheClient);
PendingDevicesManager pendingDevicesManager = new PendingDevicesManager (pendingDevices, cacheClient );
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
FederatedClientManager federatedClientManager = new FederatedClientManager(environment, config.getJerseyClientConfiguration(), config.getFederationConfiguration());
MessagesCache messagesCache = new MessagesCache(messagesClient, messages, accountsManager, config.getMessageCacheConfiguration().getPersistDelayMinutes());
MessagesManager messagesManager = new MessagesManager(messages, messagesCache);
DeadLetterHandler deadLetterHandler = new DeadLetterHandler(messagesManager);
@ -173,7 +171,6 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
GCMSender gcmSender = new GCMSender(accountsManager, config.getGcmConfiguration().getApiKey(), directoryQueue);
WebsocketSender websocketSender = new WebsocketSender(messagesManager, pubSubManager);
AccountAuthenticator deviceAuthenticator = new AccountAuthenticator(accountsManager );
FederatedPeerAuthenticator federatedPeerAuthenticator = new FederatedPeerAuthenticator(config.getFederationConfiguration());
RateLimiters rateLimiters = new RateLimiters(config.getLimitsConfiguration(), cacheClient);
ApnFallbackManager apnFallbackManager = new ApnFallbackManager(pushSchedulerClient, apnSender, accountsManager);
@ -181,7 +178,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
SmsSender smsSender = new SmsSender(twilioSmsSender);
UrlSigner urlSigner = new UrlSigner(config.getAttachmentsConfiguration());
PushSender pushSender = new PushSender(apnFallbackManager, gcmSender, apnSender, websocketSender, config.getPushConfiguration().getQueueSize());
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender, federatedClientManager);
ReceiptSender receiptSender = new ReceiptSender(accountsManager, pushSender);
TurnTokenGenerator turnTokenGenerator = new TurnTokenGenerator(config.getTurnConfiguration());
DirectoryCredentialsGenerator directoryCredentialsGenerator = new DirectoryCredentialsGenerator(config.getDirectoryConfiguration().getDirectoryClientConfiguration().getUserAuthenticationTokenSharedSecret(),
@ -201,27 +198,21 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.lifecycle().manage(messagesCache);
environment.lifecycle().manage(directoryReconciler);
AttachmentController attachmentController = new AttachmentController(rateLimiters, federatedClientManager, urlSigner);
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, federatedClientManager);
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager, apnFallbackManager);
AttachmentController attachmentController = new AttachmentController(rateLimiters, urlSigner);
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager);
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, apnFallbackManager);
ProfileController profileController = new ProfileController(rateLimiters , accountsManager, config.getProfilesConfiguration());
environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Account>()
.setAuthenticator(deviceAuthenticator)
.setPrincipal(Account.class)
.buildAuthFilter(),
new BasicCredentialAuthFilter.Builder<FederatedPeer>()
.setAuthenticator(federatedPeerAuthenticator)
.setPrincipal(FederatedPeer.class)
.buildAuthFilter()));
environment.jersey().register(new AuthValueFactoryProvider.Binder());
environment.jersey().register(new AuthValueFactoryProvider.Binder<>(Account.class));
environment.jersey().register(new AccountController(pendingAccountsManager, accountsManager, rateLimiters, smsSender, directoryQueue, messagesManager, turnTokenGenerator, config.getTestDevices()));
environment.jersey().register(new DeviceController(pendingDevicesManager, accountsManager, messagesManager, directoryQueue, rateLimiters, config.getMaxDevices()));
environment.jersey().register(new DirectoryController(rateLimiters, directory, directoryCredentialsGenerator));
environment.jersey().register(new FederationControllerV1(accountsManager, attachmentController, messageController));
environment.jersey().register(new FederationControllerV2(accountsManager, attachmentController, messageController, keysController));
environment.jersey().register(new ProvisioningController(rateLimiters, pushSender));
environment.jersey().register(new CertificateController(new CertificateGenerator(config.getDeliveryCertificate().getCertificate(), config.getDeliveryCertificate().getPrivateKey(), config.getDeliveryCertificate().getExpiresDays())));
environment.jersey().register(attachmentController);
environment.jersey().register(keysController);
environment.jersey().register(messageController);

View File

@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
@ -19,18 +19,19 @@ package org.whispersystems.textsecuregcm.auth;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dropwizard.simpleauth.Authenticator;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
public class AccountAuthenticator implements Authenticator<BasicCredentials, Account> {
@ -56,13 +57,13 @@ public class AccountAuthenticator implements Authenticator<BasicCredentials, Acc
Optional<Account> account = accountsManager.get(authorizationHeader.getNumber());
if (!account.isPresent()) {
return Optional.absent();
return Optional.empty();
}
Optional<Device> device = account.get().getDevice(authorizationHeader.getDeviceId());
if (!device.isPresent()) {
return Optional.absent();
return Optional.empty();
}
if (device.get().getAuthenticationCredentials().verify(basicCredentials.getPassword())) {
@ -73,9 +74,9 @@ public class AccountAuthenticator implements Authenticator<BasicCredentials, Acc
}
authenticationFailedMeter.mark();
return Optional.absent();
return Optional.empty();
} catch (InvalidAuthorizationHeaderException iahe) {
return Optional.absent();
return Optional.empty();
}
}

View File

@ -0,0 +1,24 @@
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.io.IOException;
public class Anonymous {
private final byte[] unidentifiedSenderAccessKey;
public Anonymous(String header) {
try {
this.unidentifiedSenderAccessKey = Base64.decode(header);
} catch (IOException e) {
throw new WebApplicationException(e, Response.Status.UNAUTHORIZED);
}
}
public byte[] getAccessKey() {
return unidentifiedSenderAccessKey;
}
}

View File

@ -0,0 +1,50 @@
package org.whispersystems.textsecuregcm.auth;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.whispersystems.textsecuregcm.crypto.Curve;
import org.whispersystems.textsecuregcm.crypto.ECPrivateKey;
import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate;
import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Base64;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.util.concurrent.TimeUnit;
public class CertificateGenerator {
private final ECPrivateKey privateKey;
private final int expiresDays;
private final ServerCertificate serverCertificate;
public CertificateGenerator(byte[] serverCertificate, ECPrivateKey privateKey, int expiresDays)
throws InvalidProtocolBufferException
{
this.privateKey = privateKey;
this.expiresDays = expiresDays;
this.serverCertificate = ServerCertificate.parseFrom(serverCertificate);
}
public byte[] createFor(Account account, Device device) throws IOException, InvalidKeyException {
byte[] certificate = SenderCertificate.Certificate.newBuilder()
.setSender(account.getNumber())
.setSenderDevice(Math.toIntExact(device.getId()))
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))
.setIdentityKey(ByteString.copyFrom(Base64.decode(account.getIdentityKey())))
.setSigner(serverCertificate)
.build()
.toByteArray();
byte[] signature = Curve.calculateSignature(privateKey, certificate);
return SenderCertificate.newBuilder()
.setCertificate(ByteString.copyFrom(certificate))
.setSignature(ByteString.copyFrom(signature))
.build()
.toByteArray();
}
}

View File

@ -1,79 +0,0 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.auth;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.dropwizard.simpleauth.Authenticator;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.util.Constants;
import java.util.List;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.basic.BasicCredentials;
public class FederatedPeerAuthenticator implements Authenticator<BasicCredentials, FederatedPeer> {
private final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private final Meter authenticationFailedMeter = metricRegistry.meter(name(getClass(),
"authentication",
"failed"));
private final Meter authenticationSucceededMeter = metricRegistry.meter(name(getClass(),
"authentication",
"succeeded"));
private final Logger logger = LoggerFactory.getLogger(FederatedPeerAuthenticator.class);
private final List<FederatedPeer> peers;
public FederatedPeerAuthenticator(FederationConfiguration config) {
this.peers = config.getPeers();
}
@Override
public Optional<FederatedPeer> authenticate(BasicCredentials basicCredentials)
throws AuthenticationException
{
if (peers == null) {
authenticationFailedMeter.mark();
return Optional.absent();
}
for (FederatedPeer peer : peers) {
if (basicCredentials.getUsername().equals(peer.getName()) &&
basicCredentials.getPassword().equals(peer.getAuthenticationToken()))
{
authenticationSucceededMeter.mark();
return Optional.of(peer);
}
}
authenticationFailedMeter.mark();
return Optional.absent();
}
}

View File

@ -0,0 +1,74 @@
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Hex;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.security.MessageDigest;
import java.util.Optional;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class OptionalAccess {
public static final String UNIDENTIFIED = "Unidentified-Access-Key";
public static void verify(Optional<Account> requestAccount,
Optional<Anonymous> accessKey,
Optional<Account> targetAccount,
String deviceSelector)
{
try {
verify(requestAccount, accessKey, targetAccount);
if (!deviceSelector.equals("*")) {
long deviceId = Long.parseLong(deviceSelector);
Optional<Device> targetDevice = targetAccount.get().getDevice(deviceId);
if (targetDevice.isPresent() && targetDevice.get().isActive()) {
return;
}
if (requestAccount.isPresent()) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
} else {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
}
} catch (NumberFormatException e) {
throw new WebApplicationException(Response.status(422).build());
}
}
public static void verify(Optional<Account> requestAccount,
Optional<Anonymous> accessKey,
Optional<Account> targetAccount)
{
if (requestAccount.isPresent() && targetAccount.isPresent() && targetAccount.get().isActive()) {
return;
}
//noinspection ConstantConditions
if (requestAccount.isPresent() && (!targetAccount.isPresent() || (targetAccount.isPresent() && !targetAccount.get().isActive()))) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
if (accessKey.isPresent() && targetAccount.isPresent() && targetAccount.get().isActive() && targetAccount.get().isUnrestrictedUnidentifiedAccess()) {
return;
}
if (accessKey.isPresent() &&
targetAccount.isPresent() &&
targetAccount.get().getUnidentifiedAccessKey().isPresent() &&
targetAccount.get().isActive() &&
MessageDigest.isEqual(accessKey.get().getAccessKey(), targetAccount.get().getUnidentifiedAccessKey().get()))
{
return;
}
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
}

View File

@ -0,0 +1,27 @@
package org.whispersystems.textsecuregcm.auth;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Optional;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class UnidentifiedAccessChecksum {
public static String generateFor(Optional<byte[]> unidentifiedAccessKey) {
try {
if (!unidentifiedAccessKey.isPresent()|| unidentifiedAccessKey.get().length != 16) return null;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(unidentifiedAccessKey.get(), "HmacSHA256"));
return Base64.encodeBytes(mac.doFinal(new byte[32]));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
}

View File

@ -1,40 +0,0 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import java.util.List;
public class FederationConfiguration {
@JsonProperty
private List<FederatedPeer> peers;
@JsonProperty
private String name;
public List<FederatedPeer> getPeers() {
return peers;
}
public String getName() {
return name;
}
}

View File

@ -1,20 +0,0 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Optional;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
public class RedPhoneConfiguration {
@JsonProperty
private String authKey;
public Optional<byte[]> getAuthorizationKey() throws DecoderException {
if (authKey == null || authKey.trim().length() == 0) {
return Optional.absent();
}
return Optional.of(Hex.decodeHex(authKey.toCharArray()));
}
}

View File

@ -0,0 +1,42 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.textsecuregcm.crypto.Curve;
import org.whispersystems.textsecuregcm.crypto.ECPrivateKey;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class UnidentifiedDeliveryConfiguration {
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotNull
private byte[] certificate;
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotNull
@Size(min = 32, max = 32)
private byte[] privateKey;
@NotNull
private int expiresDays;
public byte[] getCertificate() {
return certificate;
}
public ECPrivateKey getPrivateKey() {
return Curve.decodePrivatePoint(privateKey);
}
public int getExpiresDays() {
return expiresDays;
}
}

View File

@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
@ -21,7 +21,6 @@ import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
@ -66,11 +65,13 @@ import java.io.IOException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.Auth;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/accounts")
public class AccountController {
@ -311,13 +312,14 @@ public class AccountController {
device.setFetchesMessages(attributes.getFetchesMessages());
device.setName(attributes.getName());
device.setLastSeen(Util.todayInMillis());
device.setVoiceSupported(attributes.getVoice());
device.setVideoSupported(attributes.getVideo());
device.setUnauthenticatedDeliverySupported(attributes.getUnidentifiedAccessKey() != null);
device.setRegistrationId(attributes.getRegistrationId());
device.setSignalingKey(attributes.getSignalingKey());
device.setUserAgent(userAgent);
account.setPin(attributes.getPin());
account.setUnidentifiedAccessKey(attributes.getUnidentifiedAccessKey());
account.setUnrestrictedUnidentifiedAccess(attributes.isUnrestrictedUnidentifiedAccess());
accounts.update(account);
}
@ -339,8 +341,7 @@ public class AccountController {
device.setFetchesMessages(accountAttributes.getFetchesMessages());
device.setRegistrationId(accountAttributes.getRegistrationId());
device.setName(accountAttributes.getName());
device.setVoiceSupported(accountAttributes.getVoice());
device.setVideoSupported(accountAttributes.getVideo());
device.setUnauthenticatedDeliverySupported(accountAttributes.getUnidentifiedAccessKey() != null);
device.setCreated(System.currentTimeMillis());
device.setLastSeen(Util.todayInMillis());
device.setUserAgent(userAgent);
@ -349,6 +350,8 @@ public class AccountController {
account.setNumber(number);
account.addDevice(device);
account.setPin(accountAttributes.getPin());
account.setUnidentifiedAccessKey(accountAttributes.getUnidentifiedAccessKey());
account.setUnrestrictedUnidentifiedAccess(accountAttributes.isUnrestrictedUnidentifiedAccess());
if (accounts.create(account)) {
newUserMeter.mark();

View File

@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
@ -18,26 +18,20 @@ package org.whispersystems.textsecuregcm.controllers;
import com.amazonaws.HttpMethod;
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.AttachmentDescriptor;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.UrlSigner;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Conversions;
import org.whispersystems.textsecuregcm.s3.UrlSigner;
import javax.ws.rs.GET;
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.io.IOException;
import java.net.URL;
import java.security.SecureRandom;
@ -49,21 +43,18 @@ import io.dropwizard.auth.Auth;
@Path("/v1/attachments")
public class AttachmentController {
@SuppressWarnings("unused")
private final Logger logger = LoggerFactory.getLogger(AttachmentController.class);
private static final String[] UNACCELERATED_REGIONS = {"+20", "+971", "+968", "+974"};
private final RateLimiters rateLimiters;
private final FederatedClientManager federatedClientManager;
private final UrlSigner urlSigner;
private final RateLimiters rateLimiters;
private final UrlSigner urlSigner;
public AttachmentController(RateLimiters rateLimiters,
FederatedClientManager federatedClientManager,
UrlSigner urlSigner)
public AttachmentController(RateLimiters rateLimiters, UrlSigner urlSigner)
{
this.rateLimiters = rateLimiters;
this.federatedClientManager = federatedClientManager;
this.urlSigner = urlSigner;
this.rateLimiters = rateLimiters;
this.urlSigner = urlSigner;
}
@Timed
@ -88,20 +79,10 @@ public class AttachmentController {
@Produces(MediaType.APPLICATION_JSON)
@Path("/{attachmentId}")
public AttachmentUri redirectToAttachment(@Auth Account account,
@PathParam("attachmentId") long attachmentId,
@QueryParam("relay") Optional<String> relay)
@PathParam("attachmentId") long attachmentId)
throws IOException
{
try {
if (!relay.isPresent()) {
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region))));
} else {
return new AttachmentUri(federatedClientManager.getClient(relay.get()).getSignedAttachmentUri(attachmentId));
}
} catch (NoSuchPeerException e) {
logger.info("No such peer: " + relay);
throw new WebApplicationException(Response.status(404).build());
}
return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region))));
}
private long generateAttachmentId() {

View File

@ -0,0 +1,36 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
import org.whispersystems.textsecuregcm.storage.Account;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.security.InvalidKeyException;
import io.dropwizard.auth.Auth;
@Path("/v1/certificate")
public class CertificateController {
private final CertificateGenerator certificateGenerator;
public CertificateController(CertificateGenerator certificateGenerator) {
this.certificateGenerator = certificateGenerator;
}
@Timed
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/delivery")
public DeliveryCertificate getDeliveryCertificate(@Auth Account account) throws IOException, InvalidKeyException {
if (!account.getAuthenticatedDevice().isPresent()) throw new AssertionError();
return new DeliveryCertificate(certificateGenerator.createFor(account, account.getAuthenticatedDevice().get()));
}
}

View File

@ -18,7 +18,6 @@ package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
@ -55,6 +54,7 @@ import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import io.dropwizard.auth.Auth;
@ -213,6 +213,15 @@ public class DeviceController {
}
}
@Timed
@PUT
@Path("/unauthenticated_delivery")
public void setUnauthenticatedDelivery(@Auth Account account) {
assert(account.getAuthenticatedDevice().isPresent());
account.getAuthenticatedDevice().get().setUnauthenticatedDeliverySupported(true);
accounts.update(account);
}
@VisibleForTesting protected VerificationCode generateVerificationCode() {
SecureRandom random = new SecureRandom();
int randomInt = 100000 + random.nextInt(900000);

View File

@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
@ -20,13 +20,9 @@ import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.apache.commons.codec.DecoderException;
import org.hibernate.validator.constraints.NotEmpty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
import org.whispersystems.textsecuregcm.entities.ClientContacts;
@ -49,6 +45,7 @@ import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.auth.Auth;

View File

@ -1,19 +0,0 @@
package org.whispersystems.textsecuregcm.controllers;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
public class FederationController {
protected final AccountsManager accounts;
protected final AttachmentController attachmentController;
protected final MessageController messageController;
public FederationController(AccountsManager accounts,
AttachmentController attachmentController,
MessageController messageController)
{
this.accounts = accounts;
this.attachmentController = attachmentController;
this.messageController = messageController;
}
}

View File

@ -1,123 +0,0 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.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.AccountCount;
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.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.Util;
import javax.validation.Valid;
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.core.MediaType;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v1/federation")
public class FederationControllerV1 extends FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationControllerV1.class);
private static final int ACCOUNT_CHUNK_SIZE = 10000;
public FederationControllerV1(AccountsManager accounts,
AttachmentController attachmentController,
MessageController messageController)
{
super(accounts, attachmentController, messageController);
}
@Timed
@GET
@Path("/attachment/{attachmentId}")
@Produces(MediaType.APPLICATION_JSON)
public AttachmentUri getSignedAttachmentUri(@Auth FederatedPeer peer,
@PathParam("attachmentId") long attachmentId)
throws IOException
{
return attachmentController.redirectToAttachment(new NonLimitedAccount("Unknown", -1, peer.getName()),
attachmentId, Optional.<String>absent());
}
@Timed
@PUT
@Path("/messages/{source}/{sourceDeviceId}/{destination}")
public void sendMessages(@Auth FederatedPeer peer,
@PathParam("source") String source,
@PathParam("sourceDeviceId") long sourceDeviceId,
@PathParam("destination") String destination,
@Valid IncomingMessageList messages)
throws IOException
{
try {
messages.setRelay(null);
messageController.sendMessage(new NonLimitedAccount(source, sourceDeviceId, peer.getName()), destination, messages);
} catch (RateLimitExceededException e) {
logger.warn("Rate limiting on federated channel", e);
throw new IOException(e);
}
}
@Timed
@GET
@Path("/user_count")
@Produces(MediaType.APPLICATION_JSON)
public AccountCount getUserCount(@Auth FederatedPeer peer) {
return new AccountCount((int)accounts.getCount());
}
@Timed
@GET
@Path("/user_tokens/{offset}")
@Produces(MediaType.APPLICATION_JSON)
public ClientContacts getUserTokens(@Auth FederatedPeer peer,
@PathParam("offset") int offset)
{
List<Account> accountList = accounts.getAll(offset, ACCOUNT_CHUNK_SIZE);
List<ClientContact> clientContacts = new LinkedList<>();
for (Account account : accountList) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported(), account.isVideoSupported());
if (!account.isActive()) {
clientContact.setInactive(true);
}
clientContacts.add(clientContact);
}
return new ClientContacts(clientContacts);
}
}

View File

@ -1,51 +0,0 @@
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.PreKeyResponse;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.federation.NonLimitedAccount;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import io.dropwizard.auth.Auth;
@Path("/v2/federation")
public class FederationControllerV2 extends FederationController {
private final Logger logger = LoggerFactory.getLogger(FederationControllerV2.class);
private final KeysController keysController;
public FederationControllerV2(AccountsManager accounts, AttachmentController attachmentController, MessageController messageController, KeysController keysController) {
super(accounts, attachmentController, messageController);
this.keysController = keysController;
}
@Timed
@GET
@Path("/key/{number}/{device}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponse> getKeysV2(@Auth FederatedPeer peer,
@PathParam("number") String number,
@PathParam("device") String device)
throws IOException
{
try {
return keysController.getDeviceKeys(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);
}
}
}

View File

@ -17,18 +17,17 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.skife.jdbi.v2.exceptions.UnableToExecuteStatementException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
import org.whispersystems.textsecuregcm.entities.PreKeyState;
import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
import org.whispersystems.textsecuregcm.entities.PreKeyState;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
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;
@ -39,36 +38,34 @@ import org.whispersystems.textsecuregcm.storage.Keys;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
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 java.util.Optional;
import io.dropwizard.auth.Auth;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v2/keys")
public class KeysController {
private static final Logger logger = LoggerFactory.getLogger(KeysController.class);
private final RateLimiters rateLimiters;
private final Keys keys;
private final AccountsManager accounts;
private final FederatedClientManager federatedClientManager;
private final RateLimiters rateLimiters;
private final Keys keys;
private final AccountsManager accounts;
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts,
FederatedClientManager federatedClientManager)
{
this.rateLimiters = rateLimiters;
this.keys = keys;
this.accounts = accounts;
this.federatedClientManager = federatedClientManager;
public KeysController(RateLimiters rateLimiters, Keys keys, AccountsManager accounts) {
this.rateLimiters = rateLimiters;
this.keys = keys;
this.accounts = accounts;
}
@GET
@ -111,50 +108,49 @@ public class KeysController {
@GET
@Path("/{number}/{device_id}")
@Produces(MediaType.APPLICATION_JSON)
public Optional<PreKeyResponse> getDeviceKeys(@Auth Account account,
@PathParam("number") String number,
@PathParam("device_id") String deviceId,
@QueryParam("relay") Optional<String> relay)
public Optional<PreKeyResponse> getDeviceKeys(@Auth Optional<Account> account,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@PathParam("number") String number,
@PathParam("device_id") String deviceId)
throws RateLimitExceededException
{
try {
if (relay.isPresent()) {
return federatedClientManager.getClient(relay.get()).getKeysV2(number, deviceId);
}
if (!account.isPresent() && !accessKey.isPresent()) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
Account target = getAccount(number, deviceId);
if (account.isPresent()) {
rateLimiters.getPreKeysLimiter().validate(account.get().getNumber() + "__" + number + "." + deviceId);
}
if (account.isRateLimited()) {
rateLimiters.getPreKeysLimiter().validate(account.getNumber() + "__" + number + "." + deviceId);
}
Optional<Account> target = accounts.get(number);
OptionalAccess.verify(account, accessKey, target, deviceId);
Optional<List<KeyRecord>> targetKeys = getLocalKeys(target, deviceId);
List<PreKeyResponseItem> devices = new LinkedList<>();
assert(target.isPresent());
for (Device device : target.getDevices()) {
if (device.isActive() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
SignedPreKey signedPreKey = device.getSignedPreKey();
PreKey preKey = null;
Optional<List<KeyRecord>> targetKeys = getLocalKeys(target.get(), deviceId);
List<PreKeyResponseItem> devices = new LinkedList<>();
if (targetKeys.isPresent()) {
for (KeyRecord keyRecord : targetKeys.get()) {
if (!keyRecord.isLastResort() && keyRecord.getDeviceId() == device.getId()) {
preKey = new PreKey(keyRecord.getKeyId(), keyRecord.getPublicKey());
}
for (Device device : target.get().getDevices()) {
if (device.isActive() && (deviceId.equals("*") || device.getId() == Long.parseLong(deviceId))) {
SignedPreKey signedPreKey = device.getSignedPreKey();
PreKey preKey = null;
if (targetKeys.isPresent()) {
for (KeyRecord keyRecord : targetKeys.get()) {
if (!keyRecord.isLastResort() && keyRecord.getDeviceId() == device.getId()) {
preKey = new PreKey(keyRecord.getKeyId(), keyRecord.getPublicKey());
}
}
}
if (signedPreKey != null || preKey != null) {
devices.add(new PreKeyResponseItem(device.getId(), device.getRegistrationId(), signedPreKey, preKey));
}
if (signedPreKey != null || preKey != null) {
devices.add(new PreKeyResponseItem(device.getId(), device.getRegistrationId(), signedPreKey, preKey));
}
}
if (devices.isEmpty()) return Optional.absent();
else return Optional.of(new PreKeyResponse(target.getIdentityKey(), devices));
} catch (NoSuchPeerException | NoSuchUserException e) {
throw new WebApplicationException(Response.status(404).build());
}
if (devices.isEmpty()) return Optional.empty();
else return Optional.of(new PreKeyResponse(target.get().getIdentityKey(), devices));
}
@Timed
@ -176,12 +172,10 @@ public class KeysController {
SignedPreKey signedPreKey = device.getSignedPreKey();
if (signedPreKey != null) return Optional.of(signedPreKey);
else return Optional.absent();
else return Optional.empty();
}
private Optional<List<KeyRecord>> getLocalKeys(Account destination, String deviceIdSelector)
throws NoSuchUserException
{
private Optional<List<KeyRecord>> getLocalKeys(Account destination, String deviceIdSelector) {
try {
if (deviceIdSelector.equals("*")) {
return keys.get(destination.getNumber());
@ -202,30 +196,4 @@ public class KeysController {
throw new WebApplicationException(Response.status(422).build());
}
}
private Account getAccount(String number, String deviceSelector)
throws NoSuchUserException
{
try {
Optional<Account> account = accounts.get(number);
if (!account.isPresent() || !account.get().isActive()) {
throw new NoSuchUserException("No active account");
}
if (!deviceSelector.equals("*")) {
long deviceId = Long.parseLong(deviceSelector);
Optional<Device> targetDevice = account.get().getDevice(deviceId);
if (!targetDevice.isPresent() || !targetDevice.get().isActive()) {
throw new NoSuchUserException("No active device");
}
}
return account.get();
} catch (NumberFormatException e) {
throw new WebApplicationException(Response.status(422).build());
}
}
}

View File

@ -17,10 +17,11 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import com.google.protobuf.ByteString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
@ -29,15 +30,11 @@ import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.entities.SendMessageResponse;
import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.federation.FederatedClient;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.redis.RedisOperation;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
@ -51,6 +48,7 @@ import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@ -62,10 +60,13 @@ import java.io.IOException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import io.dropwizard.auth.Auth;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/messages")
public class MessageController {
@ -74,7 +75,6 @@ public class MessageController {
private final RateLimiters rateLimiters;
private final PushSender pushSender;
private final ReceiptSender receiptSender;
private final FederatedClientManager federatedClientManager;
private final AccountsManager accountsManager;
private final MessagesManager messagesManager;
private final ApnFallbackManager apnFallbackManager;
@ -84,7 +84,6 @@ public class MessageController {
ReceiptSender receiptSender,
AccountsManager accountsManager,
MessagesManager messagesManager,
FederatedClientManager federatedClientManager,
ApnFallbackManager apnFallbackManager)
{
this.rateLimiters = rateLimiters;
@ -92,7 +91,6 @@ public class MessageController {
this.receiptSender = receiptSender;
this.accountsManager = accountsManager;
this.messagesManager = messagesManager;
this.federatedClientManager = federatedClientManager;
this.apnFallbackManager = apnFallbackManager;
}
@ -101,22 +99,41 @@ public class MessageController {
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public SendMessageResponse sendMessage(@Auth Account source,
@PathParam("destination") String destinationName,
@Valid IncomingMessageList messages)
throws IOException, RateLimitExceededException
public SendMessageResponse sendMessage(@Auth Optional<Account> source,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@PathParam("destination") String destinationName,
@Valid IncomingMessageList messages)
throws RateLimitExceededException
{
if (!source.getNumber().equals(destinationName)) {
rateLimiters.getMessagesLimiter().validate(source.getNumber() + "__" + destinationName);
if (!source.isPresent() && !accessKey.isPresent()) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
if (source.isPresent() && !source.get().getNumber().equals(destinationName)) {
rateLimiters.getMessagesLimiter().validate(source.get().getNumber() + "__" + destinationName);
}
try {
boolean isSyncMessage = source.getNumber().equals(destinationName);
boolean isSyncMessage = source.isPresent() && source.get().getNumber().equals(destinationName);
if (Util.isEmpty(messages.getRelay())) sendLocalMessage(source, destinationName, messages, isSyncMessage);
else sendRelayMessage(source, destinationName, messages, isSyncMessage);
Optional<Account> destination;
return new SendMessageResponse(!isSyncMessage && source.getActiveDeviceCount() > 1);
if (!isSyncMessage) destination = accountsManager.get(destinationName);
else destination = source;
OptionalAccess.verify(source, accessKey, destination);
validateCompleteDeviceList(destination.get(), messages.getMessages(), isSyncMessage);
validateRegistrationIds(destination.get(), messages.getMessages());
for (IncomingMessage incomingMessage : messages.getMessages()) {
Optional<Device> destinationDevice = destination.get().getDevice(incomingMessage.getDestinationDeviceId());
if (destinationDevice.isPresent()) {
sendMessage(source, destination.get(), destinationDevice.get(), messages.getTimestamp(), incomingMessage);
}
}
return new SendMessageResponse(!isSyncMessage && source.isPresent() && source.get().getActiveDeviceCount() > 1);
} catch (NoSuchUserException e) {
throw new WebApplicationException(Response.status(404).build());
} catch (MismatchedDevicesException e) {
@ -130,8 +147,6 @@ public class MessageController {
.type(MediaType.APPLICATION_JSON)
.entity(new StaleDevices(e.getStaleDevices()))
.build());
} catch (InvalidDestinationException e) {
throw new WebApplicationException(Response.status(400).build());
}
}
@ -155,7 +170,6 @@ public class MessageController {
public void removePendingMessage(@Auth Account account,
@PathParam("source") String source,
@PathParam("timestamp") long timestamp)
throws IOException
{
try {
WebSocketConnection.messageTime.update(System.currentTimeMillis() - timestamp);
@ -167,45 +181,41 @@ public class MessageController {
if (message.isPresent() && message.get().getType() != Envelope.Type.RECEIPT_VALUE) {
receiptSender.sendReceipt(account,
message.get().getSource(),
message.get().getTimestamp(),
Optional.fromNullable(message.get().getRelay()));
message.get().getTimestamp());
}
} catch (NotPushRegisteredException e) {
logger.info("User no longer push registered for delivery receipt: " + e.getMessage());
} catch (NoSuchUserException | TransientPushFailureException e) {
} catch (NoSuchUserException e) {
logger.warn("Sending delivery receipt", e);
}
}
@Timed
@DELETE
@Path("/uuid/{uuid}")
public void removePendingMessage(@Auth Account account, @PathParam("uuid") UUID uuid) {
try {
Optional<OutgoingMessageEntity> message = messagesManager.delete(account.getNumber(),
account.getAuthenticatedDevice().get().getId(),
uuid);
private void sendLocalMessage(Account source,
String destinationName,
IncomingMessageList messages,
boolean isSyncMessage)
throws NoSuchUserException, MismatchedDevicesException, StaleDevicesException
{
Account destination;
message.ifPresent(outgoingMessageEntity -> WebSocketConnection.messageTime.update(System.currentTimeMillis() - outgoingMessageEntity.getTimestamp()));
if (!isSyncMessage) destination = getDestinationAccount(destinationName);
else destination = source;
validateCompleteDeviceList(destination, messages.getMessages(), isSyncMessage);
validateRegistrationIds(destination, messages.getMessages());
for (IncomingMessage incomingMessage : messages.getMessages()) {
Optional<Device> destinationDevice = destination.getDevice(incomingMessage.getDestinationDeviceId());
if (destinationDevice.isPresent()) {
sendLocalMessage(source, destination, destinationDevice.get(), messages.getTimestamp(), incomingMessage);
if (message.isPresent() && !Util.isEmpty(message.get().getSource()) && message.get().getType() != Envelope.Type.RECEIPT_VALUE) {
receiptSender.sendReceipt(account, message.get().getSource(), message.get().getTimestamp());
}
} catch (NoSuchUserException e) {
logger.warn("Sending delivery receipt", e);
} catch (NotPushRegisteredException e) {
logger.info("User no longer push registered for delivery receipt: " + e.getMessage());
}
}
private void sendLocalMessage(Account source,
Account destinationAccount,
Device destinationDevice,
long timestamp,
IncomingMessage incomingMessage)
private void sendMessage(Optional<Account> source,
Account destinationAccount,
Device destinationDevice,
long timestamp,
IncomingMessage incomingMessage)
throws NoSuchUserException
{
try {
@ -214,9 +224,13 @@ public class MessageController {
Envelope.Builder messageBuilder = Envelope.newBuilder();
messageBuilder.setType(Envelope.Type.valueOf(incomingMessage.getType()))
.setSource(source.getNumber())
.setTimestamp(timestamp == 0 ? System.currentTimeMillis() : timestamp)
.setSourceDevice((int) source.getAuthenticatedDevice().get().getId());
.setServerTimestamp(System.currentTimeMillis());
if (source.isPresent()) {
messageBuilder.setSource(source.get().getNumber())
.setSourceDevice((int)source.get().getAuthenticatedDevice().get().getId());
}
if (messageBody.isPresent()) {
messageBuilder.setLegacyMessage(ByteString.copyFrom(messageBody.get()));
@ -226,10 +240,6 @@ public class MessageController {
messageBuilder.setContent(ByteString.copyFrom(messageContent.get()));
}
if (source.getRelay().isPresent()) {
messageBuilder.setRelay(source.getRelay().get());
}
pushSender.sendMessage(destinationAccount, destinationDevice, messageBuilder.build());
} catch (NotPushRegisteredException e) {
if (destinationDevice.isMaster()) throw new NoSuchUserException(e);
@ -237,35 +247,6 @@ public class MessageController {
}
}
private void sendRelayMessage(Account source,
String destinationName,
IncomingMessageList messages,
boolean isSyncMessage)
throws IOException, NoSuchUserException, InvalidDestinationException
{
if (isSyncMessage) throw new InvalidDestinationException("Transcript messages can't be relayed!");
try {
FederatedClient client = federatedClientManager.getClient(messages.getRelay());
client.sendMessages(source.getNumber(), source.getAuthenticatedDevice().get().getId(),
destinationName, messages);
} catch (NoSuchPeerException e) {
throw new NoSuchUserException(e);
}
}
private Account getDestinationAccount(String destination)
throws NoSuchUserException
{
Optional<Account> account = accountsManager.get(destination);
if (!account.isPresent() || !account.get().isActive()) {
throw new NoSuchUserException(destination);
}
return account.get();
}
private void validateRegistrationIds(Account account, List<IncomingMessage> messages)
throws StaleDevicesException
{
@ -326,24 +307,24 @@ public class MessageController {
}
private Optional<byte[]> getMessageBody(IncomingMessage message) {
if (Util.isEmpty(message.getBody())) return Optional.absent();
if (Util.isEmpty(message.getBody())) return Optional.empty();
try {
return Optional.of(Base64.decode(message.getBody()));
} catch (IOException ioe) {
logger.debug("Bad B64", ioe);
return Optional.absent();
return Optional.empty();
}
}
private Optional<byte[]> getMessageContent(IncomingMessage message) {
if (Util.isEmpty(message.getContent())) return Optional.absent();
if (Util.isEmpty(message.getContent())) return Optional.empty();
try {
return Optional.of(Base64.decode(message.getContent()));
} catch (IOException ioe) {
logger.debug("Bad B64", ioe);
return Optional.absent();
return Optional.empty();
}
}
}

View File

@ -7,10 +7,12 @@ import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Optional;
import org.apache.commons.codec.binary.Base64;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.valuehandling.UnwrapValidatedValue;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
import org.whispersystems.textsecuregcm.configuration.ProfilesConfiguration;
import org.whispersystems.textsecuregcm.entities.Profile;
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
@ -22,6 +24,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.util.Pair;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
@ -33,9 +36,11 @@ import javax.ws.rs.core.Response;
import java.security.SecureRandom;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Optional;
import io.dropwizard.auth.Auth;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@Path("/v1/profile")
public class ProfileController {
@ -75,22 +80,29 @@ public class ProfileController {
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/{number}")
public Profile getProfile(@Auth Account account,
@PathParam("number") String number,
@QueryParam("ca") boolean useCaCertificate)
public Profile getProfile(@Auth Optional<Account> requestAccount,
@HeaderParam(OptionalAccess.UNIDENTIFIED) Optional<Anonymous> accessKey,
@PathParam("number") String number,
@QueryParam("ca") boolean useCaCertificate)
throws RateLimitExceededException
{
rateLimiters.getProfileLimiter().validate(account.getNumber());
Optional<Account> accountProfile = accountsManager.get(number);
if (!accountProfile.isPresent()) {
throw new WebApplicationException(Response.status(404).build());
if (!requestAccount.isPresent() && !accessKey.isPresent()) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
return new Profile(accountProfile.get().getName(),
if (requestAccount.isPresent()) {
rateLimiters.getProfileLimiter().validate(requestAccount.get().getNumber());
}
Optional<Account> accountProfile = accountsManager.get(number);
OptionalAccess.verify(requestAccount, accessKey, accountProfile);
//noinspection ConstantConditions,OptionalGetWithoutIsPresent
return new Profile(accountProfile.get().getProfileName(),
accountProfile.get().getAvatar(),
accountProfile.get().getIdentityKey());
accountProfile.get().getIdentityKey(),
accountProfile.get().isUnauthenticatedDeliverySupported() ? UnidentifiedAccessChecksum.generateFor(accountProfile.get().getUnidentifiedAccessKey()) : null,
accountProfile.get().isUnrestrictedUnidentifiedAccess());
}
@Timed
@ -98,7 +110,7 @@ public class ProfileController {
@Produces(MediaType.APPLICATION_JSON)
@Path("/name/{name}")
public void setProfile(@Auth Account account, @PathParam("name") @UnwrapValidatedValue(true) @Length(min = 72,max= 72) Optional<String> name) {
account.setName(name.orNull());
account.setProfileName(name.orElse(null));
accountsManager.update(account);
}

View File

@ -0,0 +1,98 @@
package org.whispersystems.textsecuregcm.crypto;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;
import java.security.InvalidKeyException;
import static org.whispersystems.curve25519.Curve25519.BEST;
public class Curve {
public static final int DJB_TYPE = 0x05;
public static ECKeyPair generateKeyPair() {
Curve25519KeyPair keyPair = Curve25519.getInstance(BEST).generateKeyPair();
return new ECKeyPair(new DjbECPublicKey(keyPair.getPublicKey()),
new DjbECPrivateKey(keyPair.getPrivateKey()));
}
public static ECPublicKey decodePoint(byte[] bytes, int offset)
throws InvalidKeyException
{
if (bytes == null || bytes.length - offset < 1) {
throw new InvalidKeyException("No key type identifier");
}
int type = bytes[offset] & 0xFF;
switch (type) {
case Curve.DJB_TYPE:
if (bytes.length - offset < 33) {
throw new InvalidKeyException("Bad key length: " + bytes.length);
}
byte[] keyBytes = new byte[32];
System.arraycopy(bytes, offset+1, keyBytes, 0, keyBytes.length);
return new DjbECPublicKey(keyBytes);
default:
throw new InvalidKeyException("Bad key type: " + type);
}
}
public static ECPrivateKey decodePrivatePoint(byte[] bytes) {
return new DjbECPrivateKey(bytes);
}
public static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey)
throws InvalidKeyException
{
if (publicKey == null) {
throw new InvalidKeyException("public value is null");
}
if (privateKey == null) {
throw new InvalidKeyException("private value is null");
}
if (publicKey.getType() != privateKey.getType()) {
throw new InvalidKeyException("Public and private keys must be of the same type!");
}
if (publicKey.getType() == DJB_TYPE) {
return Curve25519.getInstance(BEST)
.calculateAgreement(((DjbECPublicKey) publicKey).getPublicKey(),
((DjbECPrivateKey) privateKey).getPrivateKey());
} else {
throw new InvalidKeyException("Unknown type: " + publicKey.getType());
}
}
public static byte[] calculateSignature(ECPrivateKey signingKey, byte[] message)
throws InvalidKeyException
{
if (signingKey == null || message == null) {
throw new InvalidKeyException("Values must not be null");
}
if (signingKey.getType() == DJB_TYPE) {
return Curve25519.getInstance(BEST)
.calculateSignature(((DjbECPrivateKey) signingKey).getPrivateKey(), message);
} else {
throw new InvalidKeyException("Unknown type: " + signingKey.getType());
}
}
public static boolean verifySignature(ECPublicKey signingKey, byte[] message, byte[] signature)
throws InvalidKeyException
{
if (signingKey.getType() == DJB_TYPE) {
return Curve25519.getInstance(BEST)
.verifySignature(((DjbECPublicKey) signingKey).getPublicKey(), message, signature);
} else {
throw new InvalidKeyException("Unknown type: " + signingKey.getType());
}
}
}

View File

@ -0,0 +1,24 @@
package org.whispersystems.textsecuregcm.crypto;
public class DjbECPrivateKey implements ECPrivateKey {
private final byte[] privateKey;
DjbECPrivateKey(byte[] privateKey) {
this.privateKey = privateKey;
}
@Override
public byte[] serialize() {
return privateKey;
}
@Override
public int getType() {
return Curve.DJB_TYPE;
}
public byte[] getPrivateKey() {
return privateKey;
}
}

View File

@ -0,0 +1,49 @@
package org.whispersystems.textsecuregcm.crypto;
import org.whispersystems.textsecuregcm.util.ByteUtil;
import java.math.BigInteger;
import java.util.Arrays;
public class DjbECPublicKey implements ECPublicKey {
private final byte[] publicKey;
DjbECPublicKey(byte[] publicKey) {
this.publicKey = publicKey;
}
@Override
public byte[] serialize() {
byte[] type = {Curve.DJB_TYPE};
return ByteUtil.combine(type, publicKey);
}
@Override
public int getType() {
return Curve.DJB_TYPE;
}
@Override
public boolean equals(Object other) {
if (other == null) return false;
if (!(other instanceof DjbECPublicKey)) return false;
DjbECPublicKey that = (DjbECPublicKey)other;
return Arrays.equals(this.publicKey, that.publicKey);
}
@Override
public int hashCode() {
return Arrays.hashCode(publicKey);
}
@Override
public int compareTo(ECPublicKey another) {
return new BigInteger(publicKey).compareTo(new BigInteger(((DjbECPublicKey)another).publicKey));
}
public byte[] getPublicKey() {
return publicKey;
}
}

View File

@ -0,0 +1,20 @@
package org.whispersystems.textsecuregcm.crypto;
public class ECKeyPair {
private final ECPublicKey publicKey;
private final ECPrivateKey privateKey;
ECKeyPair(ECPublicKey publicKey, ECPrivateKey privateKey) {
this.publicKey = publicKey;
this.privateKey = privateKey;
}
public ECPublicKey getPublicKey() {
return publicKey;
}
public ECPrivateKey getPrivateKey() {
return privateKey;
}
}

View File

@ -0,0 +1,7 @@
package org.whispersystems.textsecuregcm.crypto;
public interface ECPrivateKey {
public byte[] serialize();
public int getType();
}

View File

@ -0,0 +1,10 @@
package org.whispersystems.textsecuregcm.crypto;
public interface ECPublicKey extends Comparable<ECPublicKey> {
public static final int KEY_SIZE = 33;
public byte[] serialize();
public int getType();
}

View File

@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
@ -46,6 +46,12 @@ public class AccountAttributes {
@JsonProperty
private String pin;
@JsonProperty
private byte[] unidentifiedAccessKey;
@JsonProperty
private boolean unrestrictedUnidentifiedAccess;
public AccountAttributes() {}
@VisibleForTesting
@ -91,4 +97,12 @@ public class AccountAttributes {
public String getPin() {
return pin;
}
public byte[] getUnidentifiedAccessKey() {
return unidentifiedAccessKey;
}
public boolean isUnrestrictedUnidentifiedAccess() {
return unrestrictedUnidentifiedAccess;
}
}

View File

@ -0,0 +1,50 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import java.io.IOException;
public class DeliveryCertificate {
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] certificate;
public DeliveryCertificate(byte[] certificate) {
this.certificate = certificate;
}
public DeliveryCertificate() {}
@VisibleForTesting
public byte[] getCertificate() {
return certificate;
}
public static class ByteArraySerializer extends JsonSerializer<byte[]> {
@Override
public void serialize(byte[] bytes, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(Base64.encodeBytes(bytes));
}
}
public static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
return Base64.decode(jsonParser.getValueAsString());
}
}
}

View File

@ -29,9 +29,6 @@ public class IncomingMessageList {
@Valid
private List<IncomingMessage> messages;
@JsonProperty
private String relay;
@JsonProperty
private long timestamp;
@ -41,14 +38,6 @@ public class IncomingMessageList {
return messages;
}
public String getRelay() {
return relay;
}
public void setRelay(String relay) {
this.relay = relay;
}
public long getTimestamp() {
return timestamp;
}

View File

@ -3,6 +3,8 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.UUID;
public class OutgoingMessageEntity {
@JsonIgnore
@ -11,6 +13,9 @@ public class OutgoingMessageEntity {
@JsonIgnore
private boolean cached;
@JsonProperty
private UUID guid;
@JsonProperty
private int type;
@ -32,21 +37,31 @@ public class OutgoingMessageEntity {
@JsonProperty
private byte[] content;
@JsonProperty
private long serverTimestamp;
public OutgoingMessageEntity() {}
public OutgoingMessageEntity(long id, boolean cached, int type, String relay, long timestamp,
public OutgoingMessageEntity(long id, boolean cached,
UUID guid, int type, String relay, long timestamp,
String source, int sourceDevice, byte[] message,
byte[] content)
byte[] content, long serverTimestamp)
{
this.id = id;
this.cached = cached;
this.type = type;
this.relay = relay;
this.timestamp = timestamp;
this.source = source;
this.sourceDevice = sourceDevice;
this.message = message;
this.content = content;
this.id = id;
this.cached = cached;
this.guid = guid;
this.type = type;
this.relay = relay;
this.timestamp = timestamp;
this.source = source;
this.sourceDevice = sourceDevice;
this.message = message;
this.content = content;
this.serverTimestamp = serverTimestamp;
}
public UUID getGuid() {
return guid;
}
public int getType() {
@ -87,4 +102,8 @@ public class OutgoingMessageEntity {
return cached;
}
public long getServerTimestamp() {
return serverTimestamp;
}
}

View File

@ -18,12 +18,20 @@ public class Profile {
@JsonProperty
private String avatar;
@JsonProperty
private String unidentifiedAccess;
@JsonProperty
private boolean unrestrictedUnidentifiedAccess;
public Profile() {}
public Profile(String name, String avatar, String identityKey) {
this.name = name;
this.avatar = avatar;
this.identityKey = identityKey;
public Profile(String name, String avatar, String identityKey, String unidentifiedAccess, boolean unrestrictedUnidentifiedAccess) {
this.name = name;
this.avatar = avatar;
this.identityKey = identityKey;
this.unidentifiedAccess = unidentifiedAccess;
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
}
@VisibleForTesting
@ -41,4 +49,14 @@ public class Profile {
return avatar;
}
@VisibleForTesting
public String getUnidentifiedAccess() {
return unidentifiedAccess;
}
@VisibleForTesting
public boolean isUnrestrictedUnidentifiedAccess() {
return unrestrictedUnidentifiedAccess;
}
}

View File

@ -1,252 +0,0 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.federation;
import com.google.common.base.Optional;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.bouncycastle.openssl.PEMReader;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.AccountCount;
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.PreKeyResponse;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import io.dropwizard.client.JerseyClientBuilder;
import io.dropwizard.client.JerseyClientConfiguration;
import io.dropwizard.setup.Environment;
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 = "/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 static final String RECEIPT_PATH = "/v1/receipt/%s/%d/%s/%d";
private final FederatedPeer peer;
private final Client client;
public FederatedClient(Environment environment, JerseyClientConfiguration configuration,
String federationName, FederatedPeer peer)
throws IOException
{
try {
this.client = createClient(environment, configuration, federationName, peer);
this.peer = peer;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (KeyStoreException | KeyManagementException | CertificateException e) {
throw new IOException(e);
}
}
public URL getSignedAttachmentUri(long attachmentId) throws IOException {
try {
AttachmentUri response = client.target(peer.getUrl())
.path(String.format(ATTACHMENT_URI_PATH, attachmentId))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(AttachmentUri.class);
return response.getLocation();
} catch (ProcessingException e) {
logger.warn("Bad URI", e);
throw new IOException(e);
}
}
public Optional<PreKeyResponse> getKeysV2(String destination, String device) {
try {
PreKeyResponse response = client.target(peer.getUrl())
.path(String.format(PREKEY_PATH_DEVICE_V2, destination, device))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(PreKeyResponse.class);
return Optional.of(response);
} catch (ProcessingException e) {
logger.warn("PreKey", e);
return Optional.absent();
}
}
public int getUserCount() {
try {
AccountCount count = client.target(peer.getUrl())
.path(USER_COUNT_PATH)
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(AccountCount.class);
return count.getCount();
} catch (ProcessingException e) {
logger.warn("User Count", e);
return 0;
}
}
public List<ClientContact> getUserTokens(int offset) {
try {
ClientContacts contacts = client.target(peer.getUrl())
.path(String.format(USER_TOKENS_PATH, offset))
.request()
.accept(MediaType.APPLICATION_JSON_TYPE)
.get(ClientContacts.class);
return contacts.getContacts();
} catch (ProcessingException e) {
logger.warn("User Tokens", e);
return null;
}
}
public void sendMessages(String source, long sourceDeviceId, String destination, IncomingMessageList messages)
throws IOException
{
Response response = null;
try {
response = client.target(peer.getUrl())
.path(String.format(RELAY_MESSAGE_PATH, source, sourceDeviceId, destination))
.request()
.put(Entity.json(messages));
if (response.getStatus() != 200 && response.getStatus() != 204) {
if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
}
} catch (ProcessingException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
} finally {
if (response != null) response.close();
}
}
public void sendDeliveryReceipt(String source, long sourceDeviceId, String destination, long messageId)
throws IOException
{
Response response = null;
try {
response = client.target(peer.getUrl())
.path(String.format(RECEIPT_PATH, source, sourceDeviceId, destination, messageId))
.request()
.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true)
.put(Entity.entity("", MediaType.APPLICATION_JSON_TYPE));
if (response.getStatus() != 200 && response.getStatus() != 204) {
if (response.getStatus() == 411) throw new WebApplicationException(Response.status(413).build());
else throw new WebApplicationException(Response.status(response.getStatusInfo()).build());
}
} catch (ProcessingException e) {
logger.warn("sendMessage", e);
throw new IOException(e);
} finally {
if (response != null) response.close();
}
}
private Client createClient(Environment environment, JerseyClientConfiguration configuration,
String federationName, FederatedPeer peer)
throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, CertificateException
{
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
trustManagerFactory.init(initializeTrustStore(peer.getName(), peer.getCertificate()));
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, new DefaultHostnameVerifier());
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create().register("https", sslConnectionSocketFactory).build();
Client client = new JerseyClientBuilder(environment).using(configuration)
.using(registry)
.build("FederatedClient");
client.property(ClientProperties.CONNECT_TIMEOUT, 5000);
client.property(ClientProperties.READ_TIMEOUT, 10000);
client.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);
client.register(HttpAuthenticationFeature.basic(federationName, peer.getAuthenticationToken()));
return client;
}
private KeyStore initializeTrustStore(String name, String pemCertificate)
throws CertificateException
{
try {
PEMReader reader = new PEMReader(new InputStreamReader(new ByteArrayInputStream(pemCertificate.getBytes())));
X509Certificate certificate = (X509Certificate) reader.readObject();
if (certificate == null) {
throw new CertificateException("No certificate found in parsing!");
}
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
keyStore.setCertificateEntry(name, certificate);
return keyStore;
} catch (IOException | KeyStoreException e) {
throw new CertificateException(e);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public String getPeerName() {
return peer.getName();
}
}

View File

@ -1,67 +0,0 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.federation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.client.JerseyClientConfiguration;
import io.dropwizard.setup.Environment;
public class FederatedClientManager {
private final Logger logger = LoggerFactory.getLogger(FederatedClientManager.class);
private final HashMap<String, FederatedClient> clients = new HashMap<>();
public FederatedClientManager(Environment environment,
JerseyClientConfiguration clientConfig,
FederationConfiguration federationConfig)
throws IOException
{
List<FederatedPeer> peers = federationConfig.getPeers();
String identity = federationConfig.getName();
if (peers != null) {
for (FederatedPeer peer : peers) {
logger.info("Adding peer: " + peer.getName());
clients.put(peer.getName(), new FederatedClient(environment, clientConfig, identity, peer));
}
}
}
public FederatedClient getClient(String name) throws NoSuchPeerException {
FederatedClient client = clients.get(name);
if (client == null) {
throw new NoSuchPeerException(name);
}
return client;
}
public List<FederatedClient> getClients() {
return new LinkedList<>(clients.values());
}
}

View File

@ -1,66 +0,0 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.federation;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.URL;
public class FederatedPeer {
@NotEmpty
@JsonProperty
private String name;
@NotEmpty
@URL
@JsonProperty
private String url;
@NotEmpty
@JsonProperty
private String authenticationToken;
@NotEmpty
@JsonProperty
private String certificate;
public FederatedPeer() {}
public FederatedPeer(String name, String url, String authenticationToken, String certificate) {
this.name = name;
this.url = url;
this.authenticationToken = authenticationToken;
this.certificate = certificate;
}
public String getUrl() {
return url;
}
public String getName() {
return name;
}
public String getAuthenticationToken() {
return authenticationToken;
}
public String getCertificate() {
return certificate;
}
}

View File

@ -1,24 +0,0 @@
/**
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecuregcm.federation;
public class NoSuchPeerException extends Exception {
public NoSuchPeerException(String name) {
super(name);
}
}

View File

@ -1,45 +0,0 @@
package org.whispersystems.textsecuregcm.federation;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
public class NonLimitedAccount extends Account {
@JsonIgnore
private final String number;
@JsonIgnore
private final String relay;
@JsonIgnore
private final long deviceId;
public NonLimitedAccount(String number, long deviceId, String relay) {
this.number = number;
this.deviceId = deviceId;
this.relay = relay;
}
@Override
public String getNumber() {
return number;
}
@Override
public boolean isRateLimited() {
return false;
}
@Override
public Optional<String> getRelay() {
return Optional.of(relay);
}
@Override
public Optional<Device> getAuthenticatedDevice() {
return Optional.of(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis(), System.currentTimeMillis(), false, false, "NA"));
}
}

View File

@ -20,7 +20,6 @@ import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -37,6 +36,7 @@ import org.whispersystems.textsecuregcm.util.Constants;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

View File

@ -4,7 +4,6 @@ import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.RatioGauge;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.redis.LuaScript;
@ -21,6 +20,7 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.codahale.metrics.MetricRegistry.name;
@ -28,7 +28,6 @@ import io.dropwizard.lifecycle.Managed;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisException;
@SuppressWarnings("Guava")
public class ApnFallbackManager implements Managed, Runnable {
private static final Logger logger = LoggerFactory.getLogger(ApnFallbackManager.class);
@ -167,19 +166,19 @@ public class ApnFallbackManager implements Managed, Runnable {
private Optional<Pair<String, Long>> getSeparated(String encoded) {
try {
if (encoded == null) return Optional.absent();
if (encoded == null) return Optional.empty();
String[] parts = encoded.split(":");
if (parts.length != 2) {
logger.warn("Got strange encoded number: " + encoded);
return Optional.absent();
return Optional.empty();
}
return Optional.of(new Pair<>(parts[0], Long.parseLong(parts[1])));
} catch (NumberFormatException e) {
logger.warn("Badly formatted: " + encoded, e);
return Optional.absent();
return Optional.empty();
}
}

View File

@ -4,7 +4,6 @@ import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -22,6 +21,7 @@ import org.whispersystems.textsecuregcm.util.Constants;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@ -174,7 +174,7 @@ public class GCMSender implements Managed {
}
}
return Optional.absent();
return Optional.empty();
}
private void markOutboundMeter(String key) {

View File

@ -1,64 +1,33 @@
package org.whispersystems.textsecuregcm.push;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.controllers.NoSuchUserException;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.federation.NoSuchPeerException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;
public class ReceiptSender {
private final PushSender pushSender;
private final FederatedClientManager federatedClientManager;
private final AccountsManager accountManager;
private final PushSender pushSender;
private final AccountsManager accountManager;
public ReceiptSender(AccountsManager accountManager,
PushSender pushSender,
FederatedClientManager federatedClientManager)
public ReceiptSender(AccountsManager accountManager,
PushSender pushSender)
{
this.federatedClientManager = federatedClientManager;
this.accountManager = accountManager;
this.pushSender = pushSender;
this.accountManager = accountManager;
this.pushSender = pushSender;
}
public void sendReceipt(Account source, String destination,
long messageId, Optional<String> relay)
throws IOException, NoSuchUserException,
NotPushRegisteredException, TransientPushFailureException
public void sendReceipt(Account source, String destination, long messageId)
throws NoSuchUserException, NotPushRegisteredException
{
if (source.getNumber().equals(destination)) {
return;
}
if (relay.isPresent() && !relay.get().isEmpty()) {
sendRelayedReceipt(source, destination, messageId, relay.get());
} else {
sendDirectReceipt(source, destination, messageId);
}
}
private void sendRelayedReceipt(Account source, String destination, long messageId, String relay)
throws NoSuchUserException, IOException
{
try {
federatedClientManager.getClient(relay)
.sendDeliveryReceipt(source.getNumber(),
source.getAuthenticatedDevice().get().getId(),
destination, messageId);
} catch (NoSuchPeerException e) {
throw new NoSuchUserException(e);
}
}
private void sendDirectReceipt(Account source, String destination, long messageId)
throws NotPushRegisteredException, TransientPushFailureException, NoSuchUserException
{
Account destinationAccount = getDestinationAccount(destination);
Set<Device> destinationDevices = destinationAccount.getDevices();
Envelope.Builder message = Envelope.newBuilder()

View File

@ -17,13 +17,14 @@
package org.whispersystems.textsecuregcm.sms;
import com.google.common.base.Optional;
import com.twilio.sdk.TwilioRestException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Optional;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class SmsSender {
static final String SMS_IOS_VERIFICATION_TEXT = "Your Signal verification code: %s\n\nOr tap: sgnl://verify/%s";

View File

@ -19,7 +19,6 @@ package org.whispersystems.textsecuregcm.sms;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import com.twilio.sdk.TwilioRestClient;
import com.twilio.sdk.TwilioRestException;
import com.twilio.sdk.resource.factory.CallFactory;
@ -36,10 +35,12 @@ import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import static com.codahale.metrics.MetricRegistry.name;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class TwilioSmsSender {
public static final String SAY_TWIML = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
@ -81,7 +82,7 @@ public class TwilioSmsSender {
messageParams.add(new BasicNameValuePair("MessagingServiceSid", messagingServicesId));
}
if ("ios".equals(clientType.orNull())) {
if ("ios".equals(clientType.orElse(null))) {
messageParams.add(new BasicNameValuePair("Body", String.format(SmsSender.SMS_IOS_VERIFICATION_TEXT, verificationCode, verificationCode)));
} else {
messageParams.add(new BasicNameValuePair("Body", String.format(SmsSender.SMS_VERIFICATION_TEXT, verificationCode)));

View File

@ -20,13 +20,15 @@ package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import javax.security.auth.Subject;
import java.security.Principal;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class Account {
public class Account implements Principal {
public static final int MEMCACHE_VERION = 5;
@ -51,19 +53,26 @@ public class Account {
@JsonProperty
private String pin;
@JsonProperty("uak")
private byte[] unidentifiedAccessKey;
@JsonProperty("uua")
private boolean unrestrictedUnidentifiedAccess;
@JsonIgnore
private Device authenticatedDevice;
public Account() {}
@VisibleForTesting
public Account(String number, Set<Device> devices) {
this.number = number;
this.devices = devices;
public Account(String number, Set<Device> devices, byte[] unidentifiedAccessKey) {
this.number = number;
this.devices = devices;
this.unidentifiedAccessKey = unidentifiedAccessKey;
}
public Optional<Device> getAuthenticatedDevice() {
return Optional.fromNullable(authenticatedDevice);
return Optional.ofNullable(authenticatedDevice);
}
public void setAuthenticatedDevice(Device device) {
@ -84,7 +93,7 @@ public class Account {
}
public void removeDevice(long deviceId) {
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, false, false, "NA"));
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0, "NA", false));
}
public Set<Device> getDevices() {
@ -102,27 +111,11 @@ public class Account {
}
}
return Optional.absent();
return Optional.empty();
}
public boolean isVoiceSupported() {
for (Device device : devices) {
if (device.isActive() && device.isVoiceSupported()) {
return true;
}
}
return false;
}
public boolean isVideoSupported() {
for (Device device : devices) {
if (device.isActive() && device.isVideoSupported()) {
return true;
}
}
return false;
public boolean isUnauthenticatedDeliverySupported() {
return devices.stream().filter(Device::isActive).allMatch(Device::isUnauthenticatedDeliverySupported);
}
public boolean isActive() {
@ -161,7 +154,7 @@ public class Account {
}
public Optional<String> getRelay() {
return Optional.absent();
return Optional.empty();
}
public void setIdentityKey(String identityKey) {
@ -184,11 +177,11 @@ public class Account {
return lastSeen;
}
public String getName() {
public String getProfileName() {
return name;
}
public void setName(String name) {
public void setProfileName(String name) {
this.name = name;
}
@ -209,10 +202,40 @@ public class Account {
}
public Optional<String> getPin() {
return Optional.fromNullable(pin);
return Optional.ofNullable(pin);
}
public void setPin(String pin) {
this.pin = pin;
}
public Optional<byte[]> getUnidentifiedAccessKey() {
return Optional.ofNullable(unidentifiedAccessKey);
}
public void setUnidentifiedAccessKey(byte[] unidentifiedAccessKey) {
this.unidentifiedAccessKey = unidentifiedAccessKey;
}
public boolean isUnrestrictedUnidentifiedAccess() {
return unrestrictedUnidentifiedAccess;
}
public void setUnrestrictedUnidentifiedAccess(boolean unrestrictedUnidentifiedAccess) {
this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
}
// Principal implementation
@Override
@JsonIgnore
public String getName() {
return null;
}
@Override
@JsonIgnore
public boolean implies(Subject subject) {
return false;
}
}

View File

@ -19,7 +19,6 @@ package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
@ -30,6 +29,7 @@ import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import redis.clients.jedis.Jedis;
@ -79,7 +79,7 @@ public class AccountsManager {
Optional<Account> account = memcacheGet(number);
if (!account.isPresent()) {
account = Optional.fromNullable(accounts.get(number));
account = Optional.ofNullable(accounts.get(number));
if (account.isPresent()) {
memcacheSet(number, account.get());
@ -99,7 +99,7 @@ public class AccountsManager {
private void updateDirectory(Account account) {
if (account.isActive()) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported(), account.isVideoSupported());
ClientContact clientContact = new ClientContact(token, null, true, true);
directory.add(clientContact);
} else {
directory.remove(account.getNumber());
@ -123,10 +123,10 @@ public class AccountsManager {
String json = jedis.get(getKey(number));
if (json != null) return Optional.of(mapper.readValue(json, Account.class));
else return Optional.absent();
else return Optional.empty();
} catch (IOException e) {
logger.warn("AccountsManager", "Deserialization error", e);
return Optional.absent();
return Optional.empty();
}
}

View File

@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
@ -70,40 +70,36 @@ public class Device {
@JsonProperty
private long created;
@JsonProperty
private boolean voice;
@JsonProperty
private boolean video;
@JsonProperty
private String userAgent;
@JsonProperty
private boolean unauthenticatedDelivery;
public Device() {}
public Device(long id, String name, String authToken, String salt,
String signalingKey, String gcmId, String apnId,
String voipApnId, boolean fetchesMessages,
int registrationId, SignedPreKey signedPreKey,
long lastSeen, long created, boolean voice, boolean video,
String userAgent)
long lastSeen, long created, String userAgent,
boolean unauthenticatedDelivery)
{
this.id = id;
this.name = name;
this.authToken = authToken;
this.salt = salt;
this.signalingKey = signalingKey;
this.gcmId = gcmId;
this.apnId = apnId;
this.voipApnId = voipApnId;
this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId;
this.signedPreKey = signedPreKey;
this.lastSeen = lastSeen;
this.created = created;
this.voice = voice;
this.video = video;
this.userAgent = userAgent;
this.id = id;
this.name = name;
this.authToken = authToken;
this.salt = salt;
this.signalingKey = signalingKey;
this.gcmId = gcmId;
this.apnId = apnId;
this.voipApnId = voipApnId;
this.fetchesMessages = fetchesMessages;
this.registrationId = registrationId;
this.signedPreKey = signedPreKey;
this.lastSeen = lastSeen;
this.created = created;
this.userAgent = userAgent;
this.unauthenticatedDelivery = unauthenticatedDelivery;
}
public String getApnId() {
@ -170,20 +166,12 @@ public class Device {
this.name = name;
}
public boolean isVoiceSupported() {
return voice;
public boolean isUnauthenticatedDeliverySupported() {
return unauthenticatedDelivery;
}
public void setVoiceSupported(boolean voice) {
this.voice = voice;
}
public boolean isVideoSupported() {
return video;
}
public void setVideoSupported(boolean video) {
this.video = video;
public void setUnauthenticatedDeliverySupported(boolean unauthenticatedDelivery) {
this.unauthenticatedDelivery = unauthenticatedDelivery;
}
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {

View File

@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
@ -20,7 +20,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
@ -32,6 +31,7 @@ import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
@ -102,14 +102,14 @@ public class DirectoryManager {
byte[] result = jedis.hget(DIRECTORY_KEY, token);
if (result == null) {
return Optional.absent();
return Optional.empty();
}
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);
return Optional.of(new ClientContact(token, tokenValue.relay, tokenValue.voice, tokenValue.video));
} catch (IOException e) {
logger.warn("JSON Error", e);
return Optional.absent();
return Optional.empty();
}
}
@ -205,7 +205,7 @@ public class DirectoryManager {
byte[] result = response.get();
if (result == null) {
return Optional.absent();
return Optional.empty();
}
TokenValue tokenValue = objectMapper.readValue(result, TokenValue.class);

View File

@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2018 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
@ -21,8 +21,6 @@ import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import io.dropwizard.lifecycle.Managed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
@ -37,10 +35,13 @@ import javax.ws.rs.ProcessingException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.codahale.metrics.MetricRegistry.name;
import io.dropwizard.lifecycle.Managed;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class DirectoryReconciler implements Managed, Runnable {
private static final Logger logger = LoggerFactory.getLogger(DirectoryReconciler.class);
@ -62,7 +63,6 @@ public class DirectoryReconciler implements Managed, Runnable {
private final int chunkSize;
private final long chunkIntervalMs;
private final String workerId;
private final SecureRandom random;
private boolean running;
private boolean finished;
@ -72,21 +72,15 @@ public class DirectoryReconciler implements Managed, Runnable {
DirectoryManager directoryManager,
Accounts accounts,
int chunkSize,
long chunkIntervalMs) {
long chunkIntervalMs)
{
this.accounts = accounts;
this.directoryManager = directoryManager;
this.reconciliationClient = reconciliationClient;
this.reconciliationCache = reconciliationCache;
this.chunkSize = chunkSize;
this.chunkIntervalMs = chunkIntervalMs;
this.random = new SecureRandom();
this.workerId = generateWorkerId(random);
}
private static String generateWorkerId(SecureRandom random) {
byte[] workerIdBytes = new byte[16];
random.nextBytes(workerIdBytes);
return Hex.toString(workerIdBytes);
this.workerId = Hex.toString(Util.generateSecretBytes(16));
}
@Override
@ -141,6 +135,94 @@ public class DirectoryReconciler implements Managed, Runnable {
return intervalMs;
}
private boolean processChunk() {
Optional<String> fromNumber = reconciliationCache.getLastNumber();
List<Account> chunkAccounts = readChunk(fromNumber, chunkSize);
updateDirectoryCache(chunkAccounts);
DirectoryReconciliationRequest request = createChunkRequest(fromNumber, chunkAccounts);
DirectoryReconciliationResponse sendChunkResponse = sendChunk(request);
if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.MISSING || request.getToNumber() == null) {
reconciliationCache.clearAccelerate();
}
if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.OK) {
reconciliationCache.setLastNumber(Optional.ofNullable(request.getToNumber()));
} else if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.MISSING) {
reconciliationCache.setLastNumber(Optional.empty());
}
return sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.OK;
}
private List<Account> readChunk(Optional<String> fromNumber, int chunkSize) {
try (Timer.Context timer = readChunkTimer.time()) {
Optional<List<Account>> chunkAccounts;
if (fromNumber.isPresent()) {
chunkAccounts = Optional.ofNullable(accounts.getAllFrom(fromNumber.get(), chunkSize));
} else {
chunkAccounts = Optional.ofNullable(accounts.getAllFrom(chunkSize));
}
return chunkAccounts.orElse(Collections.emptyList());
}
}
private void updateDirectoryCache(List<Account> accounts) {
if (accounts.isEmpty()) {
return;
}
BatchOperationHandle batchOperation = directoryManager.startBatchOperation();
try {
for (Account account : accounts) {
if (account.isActive()) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, true, true);
directoryManager.add(batchOperation, clientContact);
} else {
directoryManager.remove(batchOperation, account.getNumber());
}
}
} finally {
directoryManager.stopBatchOperation(batchOperation);
}
}
private DirectoryReconciliationRequest createChunkRequest(Optional<String> fromNumber, List<Account> accounts) {
List<String> numbers = accounts.stream()
.filter(Account::isActive)
.map(Account::getNumber)
.collect(Collectors.toList());
Optional<String> toNumber = Optional.empty();
if (!accounts.isEmpty()) {
toNumber = Optional.of(accounts.get(accounts.size() - 1).getNumber());
}
return new DirectoryReconciliationRequest(fromNumber.orElse(null), toNumber.orElse(null), numbers);
}
private DirectoryReconciliationResponse sendChunk(DirectoryReconciliationRequest request) {
try (Timer.Context timer = sendChunkTimer.time()) {
DirectoryReconciliationResponse response = reconciliationClient.sendChunk(request);
if (response.getStatus() != DirectoryReconciliationResponse.Status.OK) {
sendChunkErrorMeter.mark();
logger.warn("reconciliation error: " + response.getStatus());
}
return response;
} catch (ProcessingException ex) {
sendChunkErrorMeter.mark();
logger.warn("request error: ", ex);
throw new ProcessingException(ex);
}
}
private synchronized boolean sleepWhileRunning(long delayMs) {
long startTimeMs = System.currentTimeMillis();
while (running && delayMs > 0) {
@ -162,96 +244,8 @@ public class DirectoryReconciler implements Managed, Runnable {
}
private long getDelayWithJitter(long delayMs) {
long randomJitterMs = (long) (random.nextDouble() * JITTER_MAX * delayMs);
long randomJitterMs = (long) (new SecureRandom().nextDouble() * JITTER_MAX * delayMs);
return delayMs + randomJitterMs;
}
private boolean processChunk() {
Optional<String> fromNumber = reconciliationCache.getLastNumber();
List<Account> chunkAccounts = readChunk(fromNumber, chunkSize);
writeChunktoDirectoryCache(chunkAccounts);
DirectoryReconciliationRequest request = createChunkRequest(fromNumber, chunkAccounts);
DirectoryReconciliationResponse sendChunkResponse = sendChunk(request);
if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.MISSING ||
request.getToNumber() == null) {
reconciliationCache.clearAccelerate();
}
if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.OK) {
reconciliationCache.setLastNumber(Optional.fromNullable(request.getToNumber()));
} else if (sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.MISSING) {
reconciliationCache.setLastNumber(Optional.absent());
}
return sendChunkResponse.getStatus() == DirectoryReconciliationResponse.Status.OK;
}
private List<Account> readChunk(Optional<String> fromNumber, int chunkSize) {
try (Timer.Context timer = readChunkTimer.time()) {
Optional<List<Account>> chunkAccounts;
if (fromNumber.isPresent()) {
chunkAccounts = Optional.fromNullable(accounts.getAllFrom(fromNumber.get(), chunkSize));
} else {
chunkAccounts = Optional.fromNullable(accounts.getAllFrom(chunkSize));
}
return chunkAccounts.or(Collections::emptyList);
}
}
private void writeChunktoDirectoryCache(List<Account> accounts) {
if (accounts.isEmpty()) {
return;
}
BatchOperationHandle batchOperation = directoryManager.startBatchOperation();
try {
for (Account account : accounts) {
if (account.isActive()) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported(), account.isVideoSupported());
directoryManager.add(batchOperation, clientContact);
} else {
directoryManager.remove(batchOperation, account.getNumber());
}
}
} finally {
directoryManager.stopBatchOperation(batchOperation);
}
}
private DirectoryReconciliationRequest createChunkRequest(Optional<String> fromNumber, List<Account> accounts) {
List<String> numbers = accounts.stream()
.filter(Account::isActive)
.map(Account::getNumber)
.collect(Collectors.toList());
Optional<String> toNumber = Optional.absent();
if (!accounts.isEmpty()) {
toNumber = Optional.of(accounts.get(accounts.size() - 1).getNumber());
}
return new DirectoryReconciliationRequest(fromNumber.orNull(), toNumber.orNull(), numbers);
}
private DirectoryReconciliationResponse sendChunk(DirectoryReconciliationRequest request) {
try (Timer.Context timer = sendChunkTimer.time()) {
DirectoryReconciliationResponse response = reconciliationClient.sendChunk(request);
if (response.getStatus() != DirectoryReconciliationResponse.Status.OK) {
sendChunkErrorMeter.mark();
logger.warn("reconciliation error: " + response.getStatus());
}
return response;
} catch (ProcessingException ex) {
sendChunkErrorMeter.mark();
logger.warn("request error: ", ex);
throw new ProcessingException(ex);
}
}
}

View File

@ -16,7 +16,6 @@
*/
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.redis.LuaScript;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import redis.clients.jedis.Jedis;
@ -24,7 +23,9 @@ import redis.clients.jedis.Jedis;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class DirectoryReconciliationCache {
private static final String ACTIVE_WORKER_KEY = "directory_reconciliation_active_worker";
@ -62,7 +63,7 @@ public class DirectoryReconciliationCache {
public Optional<String> getLastNumber() {
try (Jedis jedis = jedisPool.getWriteResource()) {
return Optional.fromNullable(jedis.get(LAST_NUMBER_KEY));
return Optional.ofNullable(jedis.get(LAST_NUMBER_KEY));
}
}

View File

@ -16,7 +16,6 @@
*/
package org.whispersystems.textsecuregcm.storage;
import com.google.common.base.Optional;
import org.skife.jdbi.v2.SQLStatement;
import org.skife.jdbi.v2.StatementContext;
import org.skife.jdbi.v2.TransactionIsolationLevel;
@ -41,6 +40,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
public abstract class Keys {
@ -84,7 +84,7 @@ public abstract class Keys {
if (record != null && !record.isLastResort()) {
removeKey(record.getId());
} else if (record == null) {
return Optional.absent();
return Optional.empty();
}
List<KeyRecord> results = new LinkedList<>();
@ -106,7 +106,7 @@ public abstract class Keys {
}
if (preKeys != null) return Optional.of(preKeys);
else return Optional.absent();
else return Optional.empty();
}
@SqlUpdate("VACUUM keys")

View File

@ -21,15 +21,18 @@ import java.lang.annotation.Target;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
public abstract class Messages {
static final int RESULT_SET_CHUNK_SIZE = 100;
private static final String ID = "id";
private static final String GUID = "guid";
private static final String TYPE = "type";
private static final String RELAY = "relay";
private static final String TIMESTAMP = "timestamp";
private static final String SERVER_TIMESTAMP = "server_timestamp";
private static final String SOURCE = "source";
private static final String SOURCE_DEVICE = "source_device";
private static final String DESTINATION = "destination";
@ -37,11 +40,12 @@ public abstract class Messages {
private static final String MESSAGE = "message";
private static final String CONTENT = "content";
@SqlUpdate("INSERT INTO messages (" + TYPE + ", " + RELAY + ", " + TIMESTAMP + ", " + SOURCE + ", " + SOURCE_DEVICE + ", " + DESTINATION + ", " + DESTINATION_DEVICE + ", " + MESSAGE + ", " + CONTENT + ") " +
"VALUES (:type, :relay, :timestamp, :source, :source_device, :destination, :destination_device, :message, :content)")
abstract void store(@MessageBinder Envelope message,
@Bind("destination") String destination,
@Bind("destination_device") long destinationDevice);
@SqlUpdate("INSERT INTO messages (" + GUID + ", " + TYPE + ", " + RELAY + ", " + TIMESTAMP + ", " + SERVER_TIMESTAMP + ", " + SOURCE + ", " + SOURCE_DEVICE + ", " + DESTINATION + ", " + DESTINATION_DEVICE + ", " + MESSAGE + ", " + CONTENT + ") " +
"VALUES (:guid, :type, :relay, :timestamp, :server_timestamp, :source, :source_device, :destination, :destination_device, :message, :content)")
abstract void store(@Bind("guid") UUID guid,
@MessageBinder Envelope message,
@Bind("destination") String destination,
@Bind("destination_device") long destinationDevice);
@Mapper(MessageMapper.class)
@SqlQuery("SELECT * FROM messages WHERE " + DESTINATION + " = :destination AND " + DESTINATION_DEVICE + " = :destination_device ORDER BY " + TIMESTAMP + " ASC LIMIT " + RESULT_SET_CHUNK_SIZE)
@ -55,6 +59,10 @@ public abstract class Messages {
@Bind("source") String source,
@Bind("timestamp") long timestamp);
@Mapper(MessageMapper.class)
@SqlQuery("DELETE FROM messages WHERE "+ ID + " IN (SELECT " + ID + " FROM MESSAGES WHERE " + GUID + " = :guid AND " + DESTINATION + " = :destination ORDER BY " + ID + " LIMIT 1) RETURNING *")
abstract OutgoingMessageEntity remove(@Bind("destination") String destination, @Bind("guid") UUID guid);
@Mapper(MessageMapper.class)
@SqlUpdate("DELETE FROM messages WHERE " + ID + " = :id AND " + DESTINATION + " = :destination")
abstract void remove(@Bind("destination") String destination, @Bind("id") long id);
@ -79,6 +87,7 @@ public abstract class Messages {
int type = resultSet.getInt(TYPE);
byte[] legacyMessage = resultSet.getBytes(MESSAGE);
String guid = resultSet.getString(GUID);
if (type == Envelope.Type.RECEIPT_VALUE && legacyMessage == null) {
/// XXX - REMOVE AFTER 10/01/15
@ -87,13 +96,15 @@ public abstract class Messages {
return new OutgoingMessageEntity(resultSet.getLong(ID),
false,
guid == null ? null : UUID.fromString(guid),
type,
resultSet.getString(RELAY),
resultSet.getLong(TIMESTAMP),
resultSet.getString(SOURCE),
resultSet.getInt(SOURCE_DEVICE),
legacyMessage,
resultSet.getBytes(CONTENT));
resultSet.getBytes(CONTENT),
resultSet.getLong(SERVER_TIMESTAMP));
}
}
@ -113,8 +124,9 @@ public abstract class Messages {
sql.bind(TYPE, message.getType().getNumber());
sql.bind(RELAY, message.getRelay());
sql.bind(TIMESTAMP, message.getTimestamp());
sql.bind(SOURCE, message.getSource());
sql.bind(SOURCE_DEVICE, message.getSourceDevice());
sql.bind(SERVER_TIMESTAMP, message.getServerTimestamp());
sql.bind(SOURCE, message.hasSource() ? message.getSource() : null);
sql.bind(SOURCE_DEVICE, message.hasSourceDevice() ? message.getSourceDevice() : null);
sql.bind(MESSAGE, message.hasLegacyMessage() ? message.getLegacyMessage().toByteArray() : null);
sql.bind(CONTENT, message.hasContent() ? message.getContent().toByteArray() : null);
}
@ -122,6 +134,4 @@ public abstract class Messages {
}
}
}
}

View File

@ -4,7 +4,6 @@ import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.codahale.metrics.Timer;
import com.google.common.base.Optional;
import com.google.protobuf.InvalidProtocolBufferException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -25,7 +24,9 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@ -43,6 +44,7 @@ public class MessagesCache implements Managed {
private static final Timer insertTimer = metricRegistry.timer(name(MessagesCache.class, "insert" ));
private static final Timer removeByIdTimer = metricRegistry.timer(name(MessagesCache.class, "removeById" ));
private static final Timer removeByNameTimer = metricRegistry.timer(name(MessagesCache.class, "removeByName"));
private static final Timer removeByGuidTimer = metricRegistry.timer(name(MessagesCache.class, "removeByGuid"));
private static final Timer getTimer = metricRegistry.timer(name(MessagesCache.class, "get" ));
private static final Timer clearAccountTimer = metricRegistry.timer(name(MessagesCache.class, "clearAccount"));
private static final Timer clearDeviceTimer = metricRegistry.timer(name(MessagesCache.class, "clearDevice" ));
@ -67,11 +69,13 @@ public class MessagesCache implements Managed {
this.delayMinutes = delayMinutes;
}
public void insert(String destination, long destinationDevice, Envelope message) {
public void insert(UUID guid, String destination, long destinationDevice, Envelope message) {
message = message.toBuilder().setServerGuid(guid.toString()).build();
Timer.Context timer = insertTimer.time();
try {
insertOperation.insert(destination, destinationDevice, System.currentTimeMillis(), message);
insertOperation.insert(guid, destination, destinationDevice, System.currentTimeMillis(), message);
} finally {
timer.stop();
}
@ -103,7 +107,26 @@ public class MessagesCache implements Managed {
timer.stop();
}
return Optional.absent();
return Optional.empty();
}
public Optional<OutgoingMessageEntity> remove(String destination, long destinationDevice, UUID guid) {
Timer.Context timer = removeByGuidTimer.time();
try {
byte[] serialized = removeOperation.remove(destination, destinationDevice, guid);
if (serialized != null) {
Envelope envelope = Envelope.parseFrom(serialized);
return Optional.of(constructEntityFromEnvelope(0, envelope));
}
} catch (InvalidProtocolBufferException e) {
logger.warn("Failed to parse envelope", e);
} finally {
timer.stop();
}
return Optional.empty();
}
public List<OutgoingMessageEntity> get(String destination, long destinationDevice, int limit) {
@ -175,13 +198,15 @@ public class MessagesCache implements Managed {
private OutgoingMessageEntity constructEntityFromEnvelope(long id, Envelope envelope) {
return new OutgoingMessageEntity(id, true,
envelope.hasServerGuid() ? UUID.fromString(envelope.getServerGuid()) : null,
envelope.getType().getNumber(),
envelope.getRelay(),
envelope.getTimestamp(),
envelope.getSource(),
envelope.getSourceDevice(),
envelope.hasLegacyMessage() ? envelope.getLegacyMessage().toByteArray() : null,
envelope.hasContent() ? envelope.getContent().toByteArray() : null);
envelope.hasContent() ? envelope.getContent().toByteArray() : null,
envelope.hasServerTimestamp() ? envelope.getServerTimestamp() : 0);
}
private static class Key {
@ -247,12 +272,12 @@ public class MessagesCache implements Managed {
this.insert = LuaScript.fromResource(jedisPool, "lua/insert_item.lua");
}
public void insert(String destination, long destinationDevice, long timestamp, Envelope message) {
public void insert(UUID guid, String destination, long destinationDevice, long timestamp, Envelope message) {
Key key = new Key(destination, destinationDevice);
String sender = message.getSource() + "::" + message.getTimestamp();
String sender = message.hasSource() ? (message.getSource() + "::" + message.getTimestamp()) : "nil";
List<byte[]> keys = Arrays.asList(key.getUserMessageQueue(), key.getUserMessageQueueMetadata(), Key.getUserMessageQueueIndex());
List<byte[]> args = Arrays.asList(message.toByteArray(), String.valueOf(timestamp).getBytes(), sender.getBytes());
List<byte[]> args = Arrays.asList(message.toByteArray(), String.valueOf(timestamp).getBytes(), sender.getBytes(), guid.toString().getBytes());
insert.execute(keys, args);
}
@ -262,11 +287,13 @@ public class MessagesCache implements Managed {
private final LuaScript removeById;
private final LuaScript removeBySender;
private final LuaScript removeByGuid;
private final LuaScript removeQueue;
RemoveOperation(ReplicatedJedisPool jedisPool) throws IOException {
this.removeById = LuaScript.fromResource(jedisPool, "lua/remove_item_by_id.lua" );
this.removeBySender = LuaScript.fromResource(jedisPool, "lua/remove_item_by_sender.lua");
this.removeByGuid = LuaScript.fromResource(jedisPool, "lua/remove_item_by_guid.lua" );
this.removeQueue = LuaScript.fromResource(jedisPool, "lua/remove_queue.lua" );
}
@ -289,6 +316,15 @@ public class MessagesCache implements Managed {
return (byte[])this.removeBySender.execute(keys, args);
}
public byte[] remove(String destination, long destinationDevice, UUID guid) {
Key key = new Key(destination, destinationDevice);
List<byte[]> keys = Arrays.asList(key.getUserMessageQueue(), key.getUserMessageQueueMetadata(), Key.getUserMessageQueueIndex());
List<byte[]> args = Collections.singletonList(guid.toString().getBytes());
return (byte[])this.removeByGuid.execute(keys, args);
}
public void clear(String destination, long deviceId) {
Key key = new Key(destination, deviceId);
@ -445,8 +481,11 @@ public class MessagesCache implements Managed {
private void persistMessage(Key key, long score, byte[] message) {
try {
Envelope envelope = Envelope.parseFrom(message);
database.store(envelope, key.getAddress(), key.getDeviceId());
UUID guid = envelope.hasServerGuid() ? UUID.fromString(envelope.getServerGuid()) : null;
envelope = envelope.toBuilder().clearServerGuid().build();
database.store(guid, envelope, key.getAddress(), key.getDeviceId());
} catch (InvalidProtocolBufferException e) {
logger.error("Error parsing envelope", e);
}
@ -464,9 +503,7 @@ public class MessagesCache implements Managed {
}
}
private void notifyClients(AccountsManager accountsManager, PubSubManager pubSubManager, PushSender pushSender, Key key)
throws IOException
{
private void notifyClients(AccountsManager accountsManager, PubSubManager pubSubManager, PushSender pushSender, Key key) {
Timer.Context timer = notifyTimer.time();
try {

View File

@ -4,13 +4,14 @@ package org.whispersystems.textsecuregcm.storage;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.util.Constants;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static com.codahale.metrics.MetricRegistry.name;
@ -21,6 +22,9 @@ public class MessagesManager {
private static final Meter cacheMissByIdMeter = metricRegistry.meter(name(MessagesManager.class, "cacheMissById" ));
private static final Meter cacheHitByNameMeter = metricRegistry.meter(name(MessagesManager.class, "cacheHitByName" ));
private static final Meter cacheMissByNameMeter = metricRegistry.meter(name(MessagesManager.class, "cacheMissByName"));
private static final Meter cacheHitByGuidMeter = metricRegistry.meter(name(MessagesManager.class, "cacheHitByGuid" ));
private static final Meter cacheMissByGuidMeter = metricRegistry.meter(name(MessagesManager.class, "cacheMissByGuid"));
private final Messages messages;
private final MessagesCache messagesCache;
@ -31,7 +35,8 @@ public class MessagesManager {
}
public void insert(String destination, long destinationDevice, Envelope message) {
messagesCache.insert(destination, destinationDevice, message);
UUID guid = UUID.randomUUID();
messagesCache.insert(guid, destination, destinationDevice, message);
}
public OutgoingMessageEntityList getMessagesForDevice(String destination, long destinationDevice) {
@ -59,7 +64,7 @@ public class MessagesManager {
Optional<OutgoingMessageEntity> removed = this.messagesCache.remove(destination, destinationDevice, source, timestamp);
if (!removed.isPresent()) {
removed = Optional.fromNullable(this.messages.remove(destination, destinationDevice, source, timestamp));
removed = Optional.ofNullable(this.messages.remove(destination, destinationDevice, source, timestamp));
cacheMissByNameMeter.mark();
} else {
cacheHitByNameMeter.mark();
@ -68,6 +73,19 @@ public class MessagesManager {
return removed;
}
public Optional<OutgoingMessageEntity> delete(String destination, long deviceId, UUID guid) {
Optional<OutgoingMessageEntity> removed = this.messagesCache.remove(destination, deviceId, guid);
if (!removed.isPresent()) {
removed = Optional.ofNullable(this.messages.remove(destination, guid));
cacheMissByGuidMeter.mark();
} else {
cacheHitByGuidMeter.mark();
}
return removed;
}
public void delete(String destination, long deviceId, long id, boolean cached) {
if (cached) {
this.messagesCache.remove(destination, deviceId, id);

View File

@ -18,7 +18,6 @@ package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
@ -26,6 +25,7 @@ import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.io.IOException;
import java.util.Optional;
import redis.clients.jedis.Jedis;
@ -60,7 +60,7 @@ public class PendingAccountsManager {
Optional<StoredVerificationCode> code = memcacheGet(number);
if (!code.isPresent()) {
code = Optional.fromNullable(pendingAccounts.getCodeForNumber(number));
code = Optional.ofNullable(pendingAccounts.getCodeForNumber(number));
if (code.isPresent()) {
memcacheSet(number, code.get());
@ -82,11 +82,11 @@ public class PendingAccountsManager {
try (Jedis jedis = cacheClient.getReadResource()) {
String json = jedis.get(CACHE_PREFIX + number);
if (json == null) return Optional.absent();
if (json == null) return Optional.empty();
else return Optional.of(mapper.readValue(json, StoredVerificationCode.class));
} catch (IOException e) {
logger.warn("Error deserializing value...", e);
return Optional.absent();
return Optional.empty();
}
}

View File

@ -18,7 +18,6 @@ package org.whispersystems.textsecuregcm.storage;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
@ -26,6 +25,7 @@ import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.io.IOException;
import java.util.Optional;
import redis.clients.jedis.Jedis;
@ -59,7 +59,7 @@ public class PendingDevicesManager {
Optional<StoredVerificationCode> code = memcacheGet(number);
if (!code.isPresent()) {
code = Optional.fromNullable(pendingDevices.getCodeForNumber(number));
code = Optional.ofNullable(pendingDevices.getCodeForNumber(number));
if (code.isPresent()) {
memcacheSet(number, code.get());
@ -81,11 +81,11 @@ public class PendingDevicesManager {
try (Jedis jedis = cacheClient.getReadResource()) {
String json = jedis.get(CACHE_PREFIX + number);
if (json == null) return Optional.absent();
if (json == null) return Optional.empty();
else return Optional.of(mapper.readValue(json, StoredVerificationCode.class));
} catch (IOException e) {
logger.warn("Could not parse pending device stored verification json");
return Optional.absent();
return Optional.empty();
}
}

View File

@ -0,0 +1,21 @@
package org.whispersystems.textsecuregcm.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class ByteUtil {
public static byte[] combine(byte[]... elements) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (byte[] element : elements) {
baos.write(element);
}
return baos.toByteArray();
} catch (IOException e) {
throw new AssertionError(e);
}
}
}

View File

@ -20,6 +20,7 @@ import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -120,6 +121,12 @@ public class Util {
return parts;
}
public static byte[] generateSecretBytes(int size) {
byte[] data = new byte[size];
new SecureRandom().nextBytes(data);
return data;
}
public static void sleep(long i) {
try {
Thread.sleep(i);

View File

@ -25,9 +25,10 @@ import static com.codahale.metrics.MetricRegistry.name;
public class AuthenticatedConnectListener implements WebSocketConnectListener {
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private static final Timer durationTimer = metricRegistry.timer(name(WebSocketConnection.class, "connected_duration"));
private static final Logger logger = LoggerFactory.getLogger(WebSocketConnection.class);
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
private static final Timer durationTimer = metricRegistry.timer(name(WebSocketConnection.class, "connected_duration" ));
private static final Timer unauthenticatedDurationTimer = metricRegistry.timer(name(WebSocketConnection.class, "unauthenticated_connection_duration"));
private final PushSender pushSender;
private final ReceiptSender receiptSender;
@ -50,29 +51,34 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
@Override
public void onWebSocketConnect(WebSocketSessionContext context) {
final Account account = context.getAuthenticated(Account.class);
final Device device = account.getAuthenticatedDevice().get();
final String connectionId = String.valueOf(new SecureRandom().nextLong());
final Timer.Context timer = durationTimer.time();
final WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
final WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender,
messagesManager, account, device,
context.getClient(), connectionId);
final PubSubMessage connectMessage = PubSubMessage.newBuilder().setType(PubSubMessage.Type.CONNECTED)
.setContent(ByteString.copyFrom(connectionId.getBytes()))
.build();
if (context.getAuthenticated() != null) {
final Account account = context.getAuthenticated(Account.class);
final Device device = account.getAuthenticatedDevice().get();
final String connectionId = String.valueOf(new SecureRandom().nextLong());
final Timer.Context timer = durationTimer.time();
final WebsocketAddress address = new WebsocketAddress(account.getNumber(), device.getId());
final WebSocketConnection connection = new WebSocketConnection(pushSender, receiptSender,
messagesManager, account, device,
context.getClient(), connectionId);
final PubSubMessage connectMessage = PubSubMessage.newBuilder().setType(PubSubMessage.Type.CONNECTED)
.setContent(ByteString.copyFrom(connectionId.getBytes()))
.build();
RedisOperation.unchecked(() -> apnFallbackManager.cancel(account, device));
pubSubManager.publish(address, connectMessage);
pubSubManager.subscribe(address, connection);
RedisOperation.unchecked(() -> apnFallbackManager.cancel(account, device));
pubSubManager.publish(address, connectMessage);
pubSubManager.subscribe(address, connection);
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
@Override
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
pubSubManager.unsubscribe(address, connection);
timer.stop();
}
});
context.addListener(new WebSocketSessionContext.WebSocketEventListener() {
@Override
public void onWebSocketClose(WebSocketSessionContext context, int statusCode, String reason) {
pubSubManager.unsubscribe(address, connection);
timer.stop();
}
});
} else {
final Timer.Context timer = unauthenticatedDurationTimer.time();
context.addListener((context1, statusCode, reason) -> timer.stop());
}
}
}

View File

@ -1,6 +1,5 @@
package org.whispersystems.textsecuregcm.websocket;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -13,6 +12,8 @@ import org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
import org.whispersystems.websocket.WebSocketClient;
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
import java.util.Optional;
public class ProvisioningConnection implements DispatchChannel {
private final Logger logger = LoggerFactory.getLogger(ProvisioningConnection.class);

View File

@ -1,6 +1,5 @@
package org.whispersystems.textsecuregcm.websocket;
import com.google.common.base.Optional;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.storage.Account;
@ -10,6 +9,7 @@ import org.whispersystems.websocket.auth.WebSocketAuthenticator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import io.dropwizard.auth.basic.BasicCredentials;
@ -23,7 +23,7 @@ public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Acc
}
@Override
public Optional<Account> authenticate(UpgradeRequest request) throws AuthenticationException {
public AuthenticationResult<Account> authenticate(UpgradeRequest request) throws AuthenticationException {
try {
Map<String, List<String>> parameters = request.getParameterMap();
List<String> usernames = parameters.get("login");
@ -32,13 +32,13 @@ public class WebSocketAccountAuthenticator implements WebSocketAuthenticator<Acc
if (usernames == null || usernames.size() == 0 ||
passwords == null || passwords.size() == 0)
{
return Optional.absent();
return new AuthenticationResult<>(Optional.empty(), false);
}
BasicCredentials credentials = new BasicCredentials(usernames.get(0).replace(" ", "+"),
passwords.get(0).replace(" ", "+"));
return accountAuthenticator.authenticate(credentials);
return new AuthenticationResult<>(accountAuthenticator.authenticate(credentials), true);
} catch (io.dropwizard.auth.AuthenticationException e) {
throw new AuthenticationException(e);
}

View File

@ -3,7 +3,6 @@ package org.whispersystems.textsecuregcm.websocket;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.SharedMetricRegistries;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -21,24 +20,25 @@ import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.push.TransientPushFailureException;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.util.Constants;
import org.whispersystems.textsecuregcm.util.Util;
import org.whispersystems.websocket.WebSocketClient;
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.ws.rs.WebApplicationException;
import java.io.IOException;
import java.util.Iterator;
import java.util.Optional;
import static com.codahale.metrics.MetricRegistry.name;
import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
import static org.whispersystems.textsecuregcm.storage.PubSubProtos.PubSubMessage;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
public class WebSocketConnection implements DispatchChannel {
private static final MetricRegistry metricRegistry = SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME);
@ -82,7 +82,7 @@ public class WebSocketConnection implements DispatchChannel {
processStoredMessages();
break;
case PubSubMessage.Type.DELIVER_VALUE:
sendMessage(Envelope.parseFrom(pubSubMessage.getContent()), Optional.absent(), false);
sendMessage(Envelope.parseFrom(pubSubMessage.getContent()), Optional.empty(), false);
break;
case PubSubMessage.Type.CONNECTED_VALUE:
if (pubSubMessage.hasContent() && !new String(pubSubMessage.getContent().toByteArray()).equals(connectionId)) {
@ -112,7 +112,7 @@ public class WebSocketConnection implements DispatchChannel {
{
try {
EncryptedOutgoingMessage encryptedMessage = new EncryptedOutgoingMessage(message, device.getSignalingKey());
Optional<byte[]> body = Optional.fromNullable(encryptedMessage.toByteArray());
Optional<byte[]> body = Optional.ofNullable(encryptedMessage.toByteArray());
ListenableFuture<WebSocketResponseMessage> response = client.sendRequest("PUT", "/api/v1/message", null, body);
Futures.addCallback(response, new FutureCallback<WebSocketResponseMessage>() {
@ -158,14 +158,12 @@ public class WebSocketConnection implements DispatchChannel {
}
private void sendDeliveryReceiptFor(Envelope message) {
if (!message.hasSource()) return;
try {
receiptSender.sendReceipt(account, message.getSource(), message.getTimestamp(),
message.hasRelay() ? Optional.of(message.getRelay()) :
Optional.absent());
receiptSender.sendReceipt(account, message.getSource(), message.getTimestamp());
} catch (NoSuchUserException | NotPushRegisteredException e) {
logger.info("No longer registered " + e.getMessage());
} catch(IOException | TransientPushFailureException e) {
logger.warn("Something wrong while sending receipt", e);
} catch (WebApplicationException e) {
logger.warn("Bad federated response for receipt: " + e.getResponse().getStatus());
}
@ -179,9 +177,13 @@ public class WebSocketConnection implements DispatchChannel {
OutgoingMessageEntity message = iterator.next();
Envelope.Builder builder = Envelope.newBuilder()
.setType(Envelope.Type.valueOf(message.getType()))
.setSourceDevice(message.getSourceDevice())
.setSource(message.getSource())
.setTimestamp(message.getTimestamp());
.setTimestamp(message.getTimestamp())
.setServerTimestamp(message.getServerTimestamp());
if (!Util.isEmpty(message.getSource())) {
builder.setSource(message.getSource())
.setSourceDevice(message.getSourceDevice());
}
if (message.getMessage() != null) {
builder.setLegacyMessage(ByteString.copyFrom(message.getMessage()));
@ -199,7 +201,7 @@ public class WebSocketConnection implements DispatchChannel {
}
if (!messages.hasMore()) {
client.sendRequest("PUT", "/api/v1/queue/empty", null, Optional.<byte[]>absent());
client.sendRequest("PUT", "/api/v1/queue/empty", null, Optional.empty());
}
}

View File

@ -0,0 +1,92 @@
package org.whispersystems.textsecuregcm.workers;
import com.google.common.base.MoreObjects;
import com.google.protobuf.ByteString;
import net.sourceforge.argparse4j.impl.Arguments;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.whispersystems.curve25519.Curve25519;
import org.whispersystems.curve25519.Curve25519KeyPair;
import org.whispersystems.textsecuregcm.crypto.Curve;
import org.whispersystems.textsecuregcm.crypto.ECKeyPair;
import org.whispersystems.textsecuregcm.crypto.ECPrivateKey;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.util.Base64;
import java.io.IOException;
import java.security.InvalidKeyException;
import io.dropwizard.cli.Command;
import io.dropwizard.setup.Bootstrap;
public class CertificateCommand extends Command {
public CertificateCommand() {
super("certificate", "Generates server certificates for unidentified delivery");
}
@Override
public void configure(Subparser subparser) {
subparser.addArgument("-ca", "--ca")
.dest("ca")
.action(Arguments.storeTrue())
.setDefault(Boolean.FALSE)
.help("Generate CA parameters");
subparser.addArgument("-k", "--key")
.dest("key")
.type(String.class)
.help("The CA private signing key");
subparser.addArgument("-i", "--id")
.dest("keyId")
.type(Integer.class)
.help("The key ID to create");
}
@Override
public void run(Bootstrap<?> bootstrap, Namespace namespace) throws Exception {
if (MoreObjects.firstNonNull(namespace.getBoolean("ca"), false)) runCaCommand();
else runCertificateCommand(namespace);
}
private void runCaCommand() {
ECKeyPair keyPair = Curve.generateKeyPair();
System.out.println("Public key : " + Base64.encodeBytes(keyPair.getPublicKey().serialize()));
System.out.println("Private key: " + Base64.encodeBytes(keyPair.getPrivateKey().serialize()));
}
private void runCertificateCommand(Namespace namespace) throws IOException, InvalidKeyException {
if (namespace.getString("key") == null) {
System.out.println("No key specified!");
return;
}
if (namespace.getInt("keyId") == null) {
System.out.print("No key id specified!");
return;
}
ECPrivateKey key = Curve.decodePrivatePoint(Base64.decode(namespace.getString("key")));
int keyId = namespace.getInt("keyId");
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] certificate = MessageProtos.ServerCertificate.Certificate.newBuilder()
.setId(keyId)
.setKey(ByteString.copyFrom(keyPair.getPublicKey().serialize()))
.build()
.toByteArray();
byte[] signature = Curve.calculateSignature(key, certificate);
byte[] signedCertificate = MessageProtos.ServerCertificate.newBuilder()
.setCertificate(ByteString.copyFrom(certificate))
.setSignature(ByteString.copyFrom(signature))
.build()
.toByteArray();
System.out.println("Certificate: " + Base64.encodeBytes(signedCertificate));
System.out.println("Private key: " + Base64.encodeBytes(keyPair.getPrivateKey().serialize()));
}
}

View File

@ -1,7 +1,6 @@
package org.whispersystems.textsecuregcm.workers;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.google.common.base.Optional;
import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
import org.skife.jdbi.v2.DBI;
@ -19,6 +18,7 @@ import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.util.Base64;
import java.security.SecureRandom;
import java.util.Optional;
import io.dropwizard.Application;
import io.dropwizard.cli.EnvironmentCommand;

View File

@ -22,7 +22,6 @@ import org.skife.jdbi.v2.DBI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.providers.RedisClientFactory;
import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool;
import org.whispersystems.textsecuregcm.storage.Accounts;
@ -30,16 +29,13 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import io.dropwizard.Application;
import io.dropwizard.cli.ConfiguredCommand;
import io.dropwizard.cli.EnvironmentCommand;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.jdbi.ImmutableListContainerFactory;
import io.dropwizard.jdbi.ImmutableSetContainerFactory;
import io.dropwizard.jdbi.OptionalContainerFactory;
import io.dropwizard.jdbi.args.OptionalArgumentFactory;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import redis.clients.jedis.JedisPool;
public class DirectoryCommand extends EnvironmentCommand<WhisperServerConfiguration> {
@ -77,14 +73,10 @@ public class DirectoryCommand extends EnvironmentCommand<WhisperServerConfigurat
ReplicatedJedisPool redisClient = new RedisClientFactory(configuration.getDirectoryConfiguration().getRedisConfiguration().getUrl(), configuration.getDirectoryConfiguration().getRedisConfiguration().getReplicaUrls()).getRedisClientPool();
DirectoryManager directory = new DirectoryManager(redisClient);
AccountsManager accountsManager = new AccountsManager(accounts, directory, cacheClient);
// FederatedClientManager federatedClientManager = new FederatedClientManager(environment,
// configuration.getJerseyClientConfiguration(),
// configuration.getFederationConfiguration());
DirectoryUpdater update = new DirectoryUpdater(accountsManager, directory);
update.updateFromLocalDatabase();
// update.updateFromPeers();
} catch (Exception ex) {
logger.warn("Directory Exception", ex);
throw new RuntimeException(ex);

View File

@ -1,4 +1,4 @@
/**
/*
* Copyright (C) 2013 Open WhisperSystems
*
* This program is free software: you can redistribute it and/or modify
@ -16,25 +16,17 @@
*/
package org.whispersystems.textsecuregcm.workers;
import com.google.common.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.entities.ClientContact;
import org.whispersystems.textsecuregcm.federation.FederatedClient;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.storage.DirectoryManager.BatchOperationHandle;
import org.whispersystems.textsecuregcm.util.Util;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import static org.whispersystems.textsecuregcm.storage.DirectoryManager.PendingClientContact;
public class DirectoryUpdater {
private static final int CHUNK_SIZE = 10000;
@ -68,7 +60,7 @@ public class DirectoryUpdater {
for (Account account : accounts) {
if (account.isActive()) {
byte[] token = Util.getContactToken(account.getNumber());
ClientContact clientContact = new ClientContact(token, null, account.isVoiceSupported(), account.isVideoSupported());
ClientContact clientContact = new ClientContact(token, null, true, true);
directory.add(batchOperation, clientContact);
contactsAdded++;

View File

@ -1,10 +1,20 @@
-- keys: queue_key, queue_metadata_key, queue_total_index
-- argv: message, current_time, sender_key
-- keys: queue_key [1], queue_metadata_key [2], queue_total_index [3]
-- argv: message [1], current_time [2], sender (possibly null) [3], guid [4]
local messageId = redis.call("HINCRBY", KEYS[2], "counter", 1)
redis.call("ZADD", KEYS[1], "NX", messageId, ARGV[1])
redis.call("HSET", KEYS[2], ARGV[3], messageId)
redis.call("HSET", KEYS[2], messageId, ARGV[3])
if ARGV[3] ~= "nil" then
redis.call("HSET", KEYS[2], ARGV[3], messageId)
end
redis.call("HSET", KEYS[2], ARGV[4], messageId)
if ARGV[3] ~= "nil" then
redis.call("HSET", KEYS[2], messageId, ARGV[3])
end
redis.call("HSET", KEYS[2], messageId .. "guid", ARGV[4])
redis.call("EXPIRE", KEYS[1], 7776000)
redis.call("EXPIRE", KEYS[2], 7776000)

View File

@ -0,0 +1,28 @@
-- keys: queue_key, queue_metadata_key, queue_index
-- argv: guid_to_remove
local messageId = redis.call("HGET", KEYS[2], ARGV[1])
if messageId then
local envelope = redis.call("ZRANGEBYSCORE", KEYS[1], messageId, messageId, "LIMIT", 0, 1)
local sender = redis.call("HGET", KEYS[2], messageId)
redis.call("ZREMRANGEBYSCORE", KEYS[1], messageId, messageId)
redis.call("HDEL", KEYS[2], ARGV[1])
redis.call("HDEL", KEYS[2], messageId .. "guid")
if sender then
redis.call("HDEL", KEYS[2], sender)
redis.call("HDEL", KEYS[2], messageId)
end
if (redis.call("ZCARD", KEYS[1]) == 0) then
redis.call("ZREM", KEYS[3], KEYS[1])
end
if envelope and next(envelope) then
return envelope[1]
end
end
return nil

View File

@ -3,12 +3,18 @@
local removedCount = redis.call("ZREMRANGEBYSCORE", KEYS[1], ARGV[1], ARGV[1])
local senderIndex = redis.call("HGET", KEYS[2], ARGV[1])
local guidIndex = redis.call("HGET", KEYS[2], ARGV[1] .. "guid")
if senderIndex then
redis.call("HDEL", KEYS[2], senderIndex)
redis.call("HDEL", KEYS[2], ARGV[1])
end
if guidIndex then
redis.call("HDEL", KEYS[2], guidIndex)
redis.call("HDEL", KEYS[2], ARGV[1] .. "guid")
end
if (redis.call("ZCARD", KEYS[1]) == 0) then
redis.call("ZREM", KEYS[3], KEYS[1])
end

View File

@ -5,11 +5,17 @@ local messageId = redis.call("HGET", KEYS[2], ARGV[1])
if messageId then
local envelope = redis.call("ZRANGEBYSCORE", KEYS[1], messageId, messageId, "LIMIT", 0, 1)
local guid = redis.call("HGET", KEYS[2], messageId .. "guid")
redis.call("ZREMRANGEBYSCORE", KEYS[1], messageId, messageId)
redis.call("HDEL", KEYS[2], ARGV[1])
redis.call("HDEL", KEYS[2], messageId)
if guid then
redis.call("HDEL", KEYS[2], guid)
redis.call("HDEL", KEYS[2], messageId .. "guid")
end
if (redis.call("ZCARD", KEYS[1]) == 0) then
redis.call("ZREM", KEYS[3], KEYS[1])
end

View File

@ -104,5 +104,21 @@
<sql>CREATE RULE bounded_message_queue AS ON INSERT TO messages DO ALSO DELETE FROM messages WHERE id IN (SELECT id FROM messages WHERE destination = NEW.destination AND destination_device = NEW.destination_device ORDER BY timestamp DESC OFFSET 1000);</sql>
</changeSet>
<changeSet id="11" author="moxie">
<addColumn tableName="messages">
<column name="guid" type="uuid"/>
</addColumn>
<addColumn tableName="messages">
<column name="server_timestamp" type="bigint"/>
</addColumn>
<dropNotNullConstraint tableName="messages" columnName="source"/>
<dropNotNullConstraint tableName="messages" columnName="source_device"/>
</changeSet>
<changeSet id="12" author="moxie" runInTransaction="false">
<sql>CREATE INDEX CONCURRENTLY guid_index ON messages (guid);</sql>
</changeSet>
</databaseChangeLog>

View File

@ -1,6 +1,6 @@
package org.whispersystems.dispatch;
import com.google.common.base.Optional;
import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExternalResource;
@ -42,7 +42,7 @@ public class DispatchManagerTest {
}
});
dispatchManager = new DispatchManager(socketFactory, Optional.<DispatchChannel>absent());
dispatchManager = new DispatchManager(socketFactory, Optional.empty());
dispatchManager.start();
}
@ -61,7 +61,7 @@ public class DispatchManagerTest {
public void testSubscribe() throws IOException {
DispatchChannel dispatchChannel = mock(DispatchChannel.class);
dispatchManager.subscribe("foo", dispatchChannel);
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.<byte[]>absent()));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
verify(dispatchChannel, timeout(1000)).onDispatchSubscribed(eq("foo"));
}
@ -72,8 +72,8 @@ public class DispatchManagerTest {
dispatchManager.subscribe("foo", dispatchChannel);
dispatchManager.unsubscribe("foo", dispatchChannel);
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.<byte[]>absent()));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, "foo", Optional.<byte[]>absent()));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.UNSUBSCRIBE, "foo", Optional.empty()));
verify(dispatchChannel, timeout(1000)).onDispatchUnsubscribed(eq("foo"));
}
@ -86,8 +86,8 @@ public class DispatchManagerTest {
dispatchManager.subscribe("foo", fooChannel);
dispatchManager.subscribe("bar", barChannel);
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.<byte[]>absent()));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "bar", Optional.<byte[]>absent()));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "foo", Optional.empty()));
pubSubReplyInputStream.write(new PubSubReply(PubSubReply.Type.SUBSCRIBE, "bar", Optional.empty()));
verify(fooChannel, timeout(1000)).onDispatchSubscribed(eq("foo"));
verify(barChannel, timeout(1000)).onDispatchSubscribed(eq("bar"));

View File

@ -0,0 +1,137 @@
package org.whispersystems.textsecuregcm.tests.auth;
import org.junit.Test;
import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.ws.rs.WebApplicationException;
import java.util.Optional;
import static junit.framework.TestCase.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class OptionalAccessTest {
@Test
public void testUnidentifiedMissingTarget() {
try {
OptionalAccess.verify(Optional.empty(), Optional.empty(), Optional.empty());
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
@Test
public void testUnidentifiedMissingTargetDevice() {
Account account = mock(Account.class);
when(account.isActive()).thenReturn(true);
when(account.getDevice(eq(10))).thenReturn(Optional.empty());
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.encodeBytes("1234".getBytes()))), Optional.of(account), "10");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
@Test
public void testUnidentifiedBadTargetDevice() {
Account account = mock(Account.class);
when(account.isActive()).thenReturn(true);
when(account.getDevice(eq(10))).thenReturn(Optional.empty());
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.encodeBytes("1234".getBytes()))), Optional.of(account), "$$");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 422);
}
}
@Test
public void testUnidentifiedBadCode() {
Account account = mock(Account.class);
when(account.isActive()).thenReturn(true);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.encodeBytes("5678".getBytes()))), Optional.of(account));
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
@Test
public void testIdentifiedMissingTarget() {
Account account = mock(Account.class);
when(account.isActive()).thenReturn(true);
try {
OptionalAccess.verify(Optional.of(account), Optional.empty(), Optional.empty());
throw new AssertionError("should fail");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 404);
}
}
@Test
public void testUnsolicitedBadTarget() {
Account account = mock(Account.class);
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(false);
when(account.isActive()).thenReturn(true);
try {
OptionalAccess.verify(Optional.empty(), Optional.empty(), Optional.of(account));
throw new AssertionError("shold fai");
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
@Test
public void testUnsolicitedGoodTarget() {
Account account = mock(Account.class);
Anonymous random = mock(Anonymous.class);
when(account.isUnrestrictedUnidentifiedAccess()).thenReturn(true);
when(account.isActive()).thenReturn(true);
OptionalAccess.verify(Optional.empty(), Optional.of(random), Optional.of(account));
}
@Test
public void testUnidentifiedGoodTarget() {
Account account = mock(Account.class);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
when(account.isActive()).thenReturn(true);
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.encodeBytes("1234".getBytes()))), Optional.of(account));
}
@Test
public void testUnidentifiedInactive() {
Account account = mock(Account.class);
when(account.getUnidentifiedAccessKey()).thenReturn(Optional.of("1234".getBytes()));
when(account.isActive()).thenReturn(false);
try {
OptionalAccess.verify(Optional.empty(), Optional.of(new Anonymous(Base64.encodeBytes("1234".getBytes()))), Optional.of(account));
throw new AssertionError();
} catch (WebApplicationException e) {
assertEquals(e.getResponse().getStatus(), 401);
}
}
@Test
public void testIdentifiedGoodTarget() {
Account source = mock(Account.class);
Account target = mock(Account.class);
when(target.isActive()).thenReturn(true);
OptionalAccess.verify(Optional.of(source), Optional.empty(), Optional.of(target));;
}
}

View File

@ -1,11 +1,9 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.controllers.AccountController;
@ -30,8 +28,10 @@ import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.anyString;
@ -59,7 +59,7 @@ public class AccountControllerTest {
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
.addProvider(new RateLimitExceededExceptionMapper())
.setMapper(SystemMapper.getMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
@ -94,8 +94,8 @@ public class AccountControllerTest {
when(accountsManager.get(eq(SENDER_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.get(eq(SENDER_OVER_PIN))).thenReturn(Optional.of(senderPinAccount));
when(accountsManager.get(eq(SENDER))).thenReturn(Optional.absent());
when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.absent());
when(accountsManager.get(eq(SENDER))).thenReturn(Optional.empty());
when(accountsManager.get(eq(SENDER_OLD))).thenReturn(Optional.empty());
doThrow(new RateLimitExceededException(SENDER_OVER_PIN)).when(pinLimiter).validate(eq(SENDER_OVER_PIN));
}
@ -110,7 +110,7 @@ public class AccountControllerTest {
assertThat(response.getStatus()).isEqualTo(200);
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.absent()), anyString());
verify(smsSender).deliverSmsVerification(eq(SENDER), eq(Optional.empty()), anyString());
}
@Test
@ -270,6 +270,17 @@ public class AccountControllerTest {
verify(AuthHelper.VALID_ACCOUNT).setPin(eq("31337"));
}
@Test
public void testSetPinUnauthorized() throws Exception {
Response response =
resources.getJerseyTest()
.target("/v1/accounts/pin/")
.request()
.put(Entity.json(new RegistrationLock("31337")));
assertThat(response.getStatus()).isEqualTo(401);
}
@Test
public void testSetShortPin() throws Exception {
Response response =

View File

@ -3,20 +3,20 @@ package org.whispersystems.textsecuregcm.tests.controllers;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.ClassRule;
import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration;
import org.whispersystems.textsecuregcm.controllers.AttachmentController;
import org.whispersystems.textsecuregcm.entities.AttachmentDescriptor;
import org.whispersystems.textsecuregcm.entities.AttachmentUri;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.s3.UrlSigner;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import java.net.MalformedURLException;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@ -24,10 +24,9 @@ import static org.mockito.Mockito.when;
public class AttachmentControllerTest {
private static AttachmentsConfiguration configuration = mock(AttachmentsConfiguration.class);
private static FederatedClientManager federatedClientManager = mock(FederatedClientManager.class );
private static RateLimiters rateLimiters = mock(RateLimiters.class );
private static RateLimiter rateLimiter = mock(RateLimiter.class );
private static AttachmentsConfiguration configuration = mock(AttachmentsConfiguration.class);
private static RateLimiters rateLimiters = mock(RateLimiters.class );
private static RateLimiter rateLimiter = mock(RateLimiter.class );
private static UrlSigner urlSigner;
@ -43,10 +42,10 @@ public class AttachmentControllerTest {
@ClassRule
public static final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
.setMapper(SystemMapper.getMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new AttachmentController(rateLimiters, federatedClientManager, urlSigner))
.addResource(new AttachmentController(rateLimiters, urlSigner))
.build();
@Test

View File

@ -0,0 +1,113 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.ClassRule;
import org.junit.Test;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.controllers.CertificateController;
import org.whispersystems.textsecuregcm.crypto.Curve;
import org.whispersystems.textsecuregcm.entities.DeliveryCertificate;
import org.whispersystems.textsecuregcm.entities.MessageProtos.SenderCertificate;
import org.whispersystems.textsecuregcm.entities.MessageProtos.ServerCertificate;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.Base64;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.Arrays;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
public class CertificateControllerTest {
private static final String caPublicKey = "BWh+UOhT1hD8bkb+MFRvb6tVqhoG8YYGCzOd7mgjo8cV";
private static final String caPrivateKey = "EO3Mnf0kfVlVnwSaqPoQnAxhnnGL1JTdXqktCKEe9Eo=";
private static final String signingCertificate = "CiUIDBIhBbTz4h1My+tt+vw+TVscgUe/DeHS0W02tPWAWbTO2xc3EkD+go4bJnU0AcnFfbOLKoiBfCzouZtDYMOVi69rE7r4U9cXREEqOkUmU2WJBjykAxWPCcSTmVTYHDw7hkSp/puG";
private static final String signingKey = "ABOxG29xrfq4E7IrW11Eg7+HBbtba9iiS0500YoBjn4=";
private static CertificateGenerator certificateGenerator;
static {
try {
certificateGenerator = new CertificateGenerator(Base64.decode(signingCertificate), Curve.decodePrivatePoint(Base64.decode(signingKey)), 1);
} catch (IOException e) {
throw new AssertionError(e);
}
}
@ClassRule
public static final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
.setMapper(SystemMapper.getMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new CertificateController(certificateGenerator))
.build();
@Test
public void testValidCertificate() throws Exception {
DeliveryCertificate certificateObject = resources.getJerseyTest()
.target("/v1/certificate/delivery")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.get(DeliveryCertificate.class);
SenderCertificate certificateHolder = SenderCertificate.parseFrom(certificateObject.getCertificate());
SenderCertificate.Certificate certificate = SenderCertificate.Certificate.parseFrom(certificateHolder.getCertificate());
ServerCertificate serverCertificateHolder = certificate.getSigner();
ServerCertificate.Certificate serverCertificate = ServerCertificate.Certificate.parseFrom(serverCertificateHolder.getCertificate());
assertTrue(Curve.verifySignature(Curve.decodePoint(serverCertificate.getKey().toByteArray(), 0), certificateHolder.getCertificate().toByteArray(), certificateHolder.getSignature().toByteArray()));
assertTrue(Curve.verifySignature(Curve.decodePoint(Base64.decode(caPublicKey), 0), serverCertificateHolder.getCertificate().toByteArray(), serverCertificateHolder.getSignature().toByteArray()));
assertEquals(certificate.getSender(), AuthHelper.VALID_NUMBER);
assertEquals(certificate.getSenderDevice(), 1L);
assertTrue(Arrays.equals(certificate.getIdentityKey().toByteArray(), Base64.decode(AuthHelper.VALID_IDENTITY)));
}
@Test
public void testBadAuthentication() throws Exception {
Response response = resources.getJerseyTest()
.target("/v1/certificate/delivery")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.INVALID_PASSWORD))
.get();
assertEquals(response.getStatus(), 401);
}
@Test
public void testNoAuthentication() throws Exception {
Response response = resources.getJerseyTest()
.target("/v1/certificate/delivery")
.request()
.get();
assertEquals(response.getStatus(), 401);
}
@Test
public void testUnidentifiedAuthentication() throws Exception {
Response response = resources.getJerseyTest()
.target("/v1/certificate/delivery")
.request()
.header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1234".getBytes()))
.get();
assertEquals(response.getStatus(), 401);
}
}

View File

@ -16,12 +16,10 @@
*/
package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
import org.whispersystems.textsecuregcm.controllers.DeviceController;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
@ -40,8 +38,10 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
@ -83,7 +83,7 @@ public class DeviceControllerTest {
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addProvider(new DeviceLimitExceededExceptionMapper())
.addResource(new DumbVerificationDeviceController(pendingDevicesManager,
@ -202,15 +202,4 @@ public class DeviceControllerTest {
verifyNoMoreInteractions(messagesManager);
}
@Test
public void removeDeviceTest() throws Exception {
Response response = resources.getJerseyTest()
.target("/v1/devices/12345")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD))
.delete();
assertEquals(204, response.getStatus());
verify(directoryQueue).deleteRegisteredUser(eq(AuthHelper.VALID_NUMBER));
}
}

View File

@ -1,21 +1,18 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.auth.DirectoryCredentials;
import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator;
import org.whispersystems.textsecuregcm.configuration.DirectoryConfiguration;
import org.whispersystems.textsecuregcm.configuration.DirectoryClientConfiguration;
import org.whispersystems.textsecuregcm.controllers.DirectoryController;
import org.whispersystems.textsecuregcm.entities.ClientContactTokens;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.DirectoryManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.Base64;
@ -26,11 +23,11 @@ import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyListOf;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Matchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -46,7 +43,7 @@ public class DirectoryControllerTest {
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new DirectoryController(rateLimiters,
directoryManager,

View File

@ -1,132 +0,0 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV1;
import org.whispersystems.textsecuregcm.controllers.FederationControllerV2;
import org.whispersystems.textsecuregcm.controllers.KeysController;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos;
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
import org.whispersystems.textsecuregcm.push.PushSender;
import org.whispersystems.textsecuregcm.push.ReceiptSender;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
import static org.whispersystems.textsecuregcm.tests.util.JsonHelpers.jsonFixture;
public class FederatedControllerTest {
private static final String SINGLE_DEVICE_RECIPIENT = "+14151111111";
private static final String MULTI_DEVICE_RECIPIENT = "+14152222222";
private PushSender pushSender = mock(PushSender.class );
private ReceiptSender receiptSender = mock(ReceiptSender.class);
private FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
private AccountsManager accountsManager = mock(AccountsManager.class );
private MessagesManager messagesManager = mock(MessagesManager.class);
private RateLimiters rateLimiters = mock(RateLimiters.class );
private RateLimiter rateLimiter = mock(RateLimiter.class );
private ApnFallbackManager apnFallbackManager = mock(ApnFallbackManager.class);
private final SignedPreKey signedPreKey = new SignedPreKey(3333, "foo", "baar");
private final PreKeyResponse preKeyResponseV2 = new PreKeyResponse("foo", new LinkedList<PreKeyResponseItem>());
private final ObjectMapper mapper = new ObjectMapper();
private final MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, federatedClientManager, apnFallbackManager);
private final KeysController keysControllerV2 = mock(KeysController.class);
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new FederationControllerV1(accountsManager, null, messageController))
.addResource(new FederationControllerV2(accountsManager, null, messageController, keysControllerV2))
.build();
@Before
public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
}};
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(222, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(333, "rad", "mad"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, multiDeviceList);
when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount));
when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount));
when(rateLimiters.getMessagesLimiter()).thenReturn(rateLimiter);
when(keysControllerV2.getSignedKey(any(Account.class))).thenReturn(Optional.of(signedPreKey));
when(keysControllerV2.getDeviceKeys(any(Account.class), anyString(), anyString(), any(Optional.class)))
.thenReturn(Optional.of(preKeyResponseV2));
}
@Test
public void testSingleDeviceCurrent() throws Exception {
Response response =
resources.getJerseyTest()
.target(String.format("/v1/federation/messages/+14152223333/1/%s", SINGLE_DEVICE_RECIPIENT))
.request()
.header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo"))
.put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
MediaType.APPLICATION_JSON_TYPE));
assertThat("Good Response", response.getStatus(), is(equalTo(204)));
verify(pushSender).sendMessage(any(Account.class), any(Device.class), any(MessageProtos.Envelope.class));
}
@Test
public void testSignedPreKeyV2() throws Exception {
PreKeyResponse response =
resources.getJerseyTest()
.target("/v2/federation/key/+14152223333/1")
.request()
.header("Authorization", AuthHelper.getAuthHeader("cyanogen", "foofoo"))
.get(PreKeyResponse.class);
assertThat("good response", response.getIdentityKey().equals(preKeyResponseV2.getIdentityKey()));
}
}

View File

@ -1,12 +1,11 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.controllers.KeysController;
import org.whispersystems.textsecuregcm.entities.PreKey;
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
@ -28,8 +27,10 @@ import javax.ws.rs.core.Response;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
@ -63,9 +64,9 @@ public class KeyControllerTest {
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new KeysController(rateLimiters, keys, accounts, null))
.addResource(new KeysController(rateLimiters, keys, accounts))
.build();
@Before
@ -103,14 +104,15 @@ public class KeyControllerTest {
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.<Device>absent());
when(existsAccount.getDevice(22L)).thenReturn(Optional.<Device>empty());
when(existsAccount.getDevices()).thenReturn(allDevices);
when(existsAccount.isActive()).thenReturn(true);
when(existsAccount.getIdentityKey()).thenReturn("existsidentitykey");
when(existsAccount.getNumber()).thenReturn(EXISTS_NUMBER);
when(existsAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of("1337".getBytes()));
when(accounts.get(EXISTS_NUMBER)).thenReturn(Optional.of(existsAccount));
when(accounts.get(NOT_EXISTS_NUMBER)).thenReturn(Optional.<Account>absent());
when(accounts.get(NOT_EXISTS_NUMBER)).thenReturn(Optional.<Account>empty());
when(rateLimiters.getPreKeysLimiter()).thenReturn(rateLimiter);
@ -118,7 +120,7 @@ public class KeyControllerTest {
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.<List<KeyRecord>>absent());
when(keys.get(eq(NOT_EXISTS_NUMBER), eq(1L))).thenReturn(Optional.<List<KeyRecord>>empty());
List<KeyRecord> multiDevice = new LinkedList<>();
multiDevice.add(SAMPLE_KEY);
@ -191,6 +193,49 @@ public class KeyControllerTest {
verifyNoMoreInteractions(keys);
}
@Test
public void testUnidentifiedRequest() throws Exception {
PreKeyResponse result = resources.getJerseyTest()
.target(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
.request()
.header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("1337".getBytes()))
.get(PreKeyResponse.class);
assertThat(result.getIdentityKey()).isEqualTo(existsAccount.getIdentityKey());
assertThat(result.getDevicesCount()).isEqualTo(1);
assertThat(result.getDevice(1).getPreKey().getKeyId()).isEqualTo(SAMPLE_KEY.getKeyId());
assertThat(result.getDevice(1).getPreKey().getPublicKey()).isEqualTo(SAMPLE_KEY.getPublicKey());
assertThat(result.getDevice(1).getSignedPreKey()).isEqualTo(existsAccount.getDevice(1).get().getSignedPreKey());
verify(keys).get(eq(EXISTS_NUMBER), eq(1L));
verifyNoMoreInteractions(keys);
}
@Test
public void testUnauthorizedUnidentifiedRequest() throws Exception {
Response response = resources.getJerseyTest()
.target(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
.request()
.header(OptionalAccess.UNIDENTIFIED, AuthHelper.getUnidentifiedAccessHeader("9999".getBytes()))
.get();
assertThat(response.getStatus()).isEqualTo(401);
verifyNoMoreInteractions(keys);
}
@Test
public void testMalformedUnidentifiedRequest() throws Exception {
Response response = resources.getJerseyTest()
.target(String.format("/v2/keys/%s/1", EXISTS_NUMBER))
.request()
.header(OptionalAccess.UNIDENTIFIED, "$$$$$$$$$")
.get();
assertThat(response.getStatus()).isEqualTo(401);
verifyNoMoreInteractions(keys);
}
@Test
public void validMultiRequestTestV2() throws Exception {
PreKeyResponse results = resources.getJerseyTest()

View File

@ -1,12 +1,12 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.mockito.ArgumentCaptor;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.controllers.MessageController;
import org.whispersystems.textsecuregcm.entities.IncomingMessageList;
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
@ -15,7 +15,6 @@ import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntity;
import org.whispersystems.textsecuregcm.entities.OutgoingMessageEntityList;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.entities.StaleDevices;
import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.push.ApnFallbackManager;
@ -26,6 +25,7 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.MessagesManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.Base64;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
@ -33,14 +33,17 @@ import javax.ws.rs.core.Response;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
@ -54,7 +57,6 @@ public class MessageControllerTest {
private final PushSender pushSender = mock(PushSender.class );
private final ReceiptSender receiptSender = mock(ReceiptSender.class);
private final FederatedClientManager federatedClientManager = mock(FederatedClientManager.class);
private final AccountsManager accountsManager = mock(AccountsManager.class );
private final MessagesManager messagesManager = mock(MessagesManager.class);
private final RateLimiters rateLimiters = mock(RateLimiters.class );
@ -66,27 +68,27 @@ public class MessageControllerTest {
@Rule
public final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new MessageController(rateLimiters, pushSender, receiptSender, accountsManager,
messagesManager, federatedClientManager, apnFallbackManager))
messagesManager, apnFallbackManager))
.build();
@Before
public void setup() throws Exception {
Set<Device> singleDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 111, new SignedPreKey(333, "baz", "boop"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true));
}};
Set<Device> multiDeviceList = new HashSet<Device>() {{
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), false, false, "Test"));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), false, false, "Test"));
add(new Device(1, null, "foo", "bar", "baz", "isgcm", null, null, false, 222, new SignedPreKey(111, "foo", "bar"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true));
add(new Device(2, null, "foo", "bar", "baz", "isgcm", null, null, false, 333, new SignedPreKey(222, "oof", "rab"), System.currentTimeMillis(), System.currentTimeMillis(), "Test", true));
add(new Device(3, null, "foo", "bar", "baz", "isgcm", null, null, false, 444, null, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31), System.currentTimeMillis(), "Test", true));
}};
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList);
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, multiDeviceList);
Account singleDeviceAccount = new Account(SINGLE_DEVICE_RECIPIENT, singleDeviceList, "1234".getBytes());
Account multiDeviceAccount = new Account(MULTI_DEVICE_RECIPIENT, multiDeviceList, "1234".getBytes());
when(accountsManager.get(eq(SINGLE_DEVICE_RECIPIENT))).thenReturn(Optional.of(singleDeviceAccount));
when(accountsManager.get(eq(MULTI_DEVICE_RECIPIENT))).thenReturn(Optional.of(multiDeviceAccount));
@ -106,7 +108,43 @@ public class MessageControllerTest {
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), any(Envelope.class));
ArgumentCaptor<Envelope> captor = ArgumentCaptor.forClass(Envelope.class);
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture());
assertTrue(captor.getValue().hasSource());
assertTrue(captor.getValue().hasSourceDevice());
}
@Test
public synchronized void testSingleDeviceCurrentUnidentified() throws Exception {
Response response =
resources.getJerseyTest()
.target(String.format("/v1/messages/%s", SINGLE_DEVICE_RECIPIENT))
.request()
.header(OptionalAccess.UNIDENTIFIED, Base64.encodeBytes("1234".getBytes()))
.put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
MediaType.APPLICATION_JSON_TYPE));
assertThat("Good Response", response.getStatus(), is(equalTo(200)));
ArgumentCaptor<Envelope> captor = ArgumentCaptor.forClass(Envelope.class);
verify(pushSender, times(1)).sendMessage(any(Account.class), any(Device.class), captor.capture());
assertFalse(captor.getValue().hasSource());
assertFalse(captor.getValue().hasSourceDevice());
}
@Test
public synchronized void testSendBadAuth() throws Exception {
Response response =
resources.getJerseyTest()
.target(String.format("/v1/messages/%s", SINGLE_DEVICE_RECIPIENT))
.request()
.put(Entity.entity(mapper.readValue(jsonFixture("fixtures/current_message_single_device.json"), IncomingMessageList.class),
MediaType.APPLICATION_JSON_TYPE));
assertThat("Good Response", response.getStatus(), is(equalTo(401)));
}
@Test
@ -187,9 +225,11 @@ public class MessageControllerTest {
final long timestampOne = 313377;
final long timestampTwo = 313388;
final UUID uuidOne = UUID.randomUUID();
List<OutgoingMessageEntity> messages = new LinkedList<OutgoingMessageEntity>() {{
add(new OutgoingMessageEntity(1L, false, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes(), null));
add(new OutgoingMessageEntity(2L, false, Envelope.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null, null));
add(new OutgoingMessageEntity(1L, false, uuidOne, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes(), null, 0));
add(new OutgoingMessageEntity(2L, false, null, Envelope.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null, null, 0));
}};
OutgoingMessageEntityList messagesList = new OutgoingMessageEntityList(messages, false);
@ -211,6 +251,9 @@ public class MessageControllerTest {
assertEquals(response.getMessages().get(0).getTimestamp(), timestampOne);
assertEquals(response.getMessages().get(1).getTimestamp(), timestampTwo);
assertEquals(response.getMessages().get(0).getGuid(), uuidOne);
assertEquals(response.getMessages().get(1).getGuid(), null);
}
@Test
@ -219,8 +262,8 @@ public class MessageControllerTest {
final long timestampTwo = 313388;
List<OutgoingMessageEntity> messages = new LinkedList<OutgoingMessageEntity>() {{
add(new OutgoingMessageEntity(1L, false, Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes(), null));
add(new OutgoingMessageEntity(2L, false, Envelope.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null, null));
add(new OutgoingMessageEntity(1L, false, UUID.randomUUID(), Envelope.Type.CIPHERTEXT_VALUE, null, timestampOne, "+14152222222", 2, "hi there".getBytes(), null, 0));
add(new OutgoingMessageEntity(2L, false, UUID.randomUUID(), Envelope.Type.RECEIPT_VALUE, null, timestampTwo, "+14152222222", 2, null, null, 0));
}};
OutgoingMessageEntityList messagesList = new OutgoingMessageEntityList(messages, false);
@ -240,21 +283,22 @@ public class MessageControllerTest {
@Test
public synchronized void testDeleteMessages() throws Exception {
long timestamp = System.currentTimeMillis();
when(messagesManager.delete(AuthHelper.VALID_NUMBER, 1, "+14152222222", 31337))
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L, true,
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L, true, null,
Envelope.Type.CIPHERTEXT_VALUE,
null, timestamp,
"+14152222222", 1, "hi".getBytes(), null)));
"+14152222222", 1, "hi".getBytes(), null, 0)));
when(messagesManager.delete(AuthHelper.VALID_NUMBER, 1, "+14152222222", 31338))
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L, true,
.thenReturn(Optional.of(new OutgoingMessageEntity(31337L, true, null,
Envelope.Type.RECEIPT_VALUE,
null, System.currentTimeMillis(),
"+14152222222", 1, null, null)));
"+14152222222", 1, null, null, 0)));
when(messagesManager.delete(AuthHelper.VALID_NUMBER, 1, "+14152222222", 31339))
.thenReturn(Optional.<OutgoingMessageEntity>absent());
.thenReturn(Optional.empty());
Response response = resources.getJerseyTest()
.target(String.format("/v1/messages/%s/%d", "+14152222222", 31337))
@ -263,7 +307,7 @@ public class MessageControllerTest {
.delete();
assertThat("Good Response Code", response.getStatus(), is(equalTo(204)));
verify(receiptSender).sendReceipt(any(Account.class), eq("+14152222222"), eq(timestamp), eq(Optional.<String>absent()));
verify(receiptSender).sendReceipt(any(Account.class), eq("+14152222222"), eq(timestamp));
response = resources.getJerseyTest()
.target(String.format("/v1/messages/%s/%d", "+14152222222", 31338))

View File

@ -1,11 +1,9 @@
package org.whispersystems.textsecuregcm.tests.controllers;
import com.google.common.base.Optional;
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.whispersystems.dropwizard.simpleauth.AuthValueFactoryProvider;
import org.whispersystems.textsecuregcm.configuration.ProfilesConfiguration;
import org.whispersystems.textsecuregcm.controllers.ProfileController;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
@ -17,6 +15,10 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
import org.whispersystems.textsecuregcm.util.SystemMapper;
import javax.ws.rs.core.Response;
import java.util.Optional;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.testing.junit.ResourceTestRule;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.mockito.Mockito.*;
@ -38,7 +40,7 @@ public class ProfileControllerTest {
@ClassRule
public static final ResourceTestRule resources = ResourceTestRule.builder()
.addProvider(AuthHelper.getAuthFilter())
.addProvider(new AuthValueFactoryProvider.Binder())
.addProvider(new AuthValueFactoryProvider.Binder<>(Account.class))
.setMapper(SystemMapper.getMapper())
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
.addResource(new ProfileController(rateLimiters,
@ -53,9 +55,10 @@ public class ProfileControllerTest {
Account profileAccount = mock(Account.class);
when(profileAccount.getIdentityKey()).thenReturn("bar");
when(profileAccount.getName()).thenReturn("baz");
when(profileAccount.getProfileName()).thenReturn("baz");
when(profileAccount.getAvatar()).thenReturn("profiles/bang");
when(profileAccount.getAvatarDigest()).thenReturn("buh");
when(profileAccount.isActive()).thenReturn(true);
when(accountsManager.get(AuthHelper.VALID_NUMBER_TWO)).thenReturn(Optional.of(profileAccount));
}
@ -78,5 +81,14 @@ public class ProfileControllerTest {
verify(rateLimiter, times(1)).validate(AuthHelper.VALID_NUMBER);
}
@Test
public void testProfileGetUnauthorized() throws Exception {
Response response = resources.getJerseyTest()
.target("/v1/profile/" + AuthHelper.VALID_NUMBER_TWO)
.request()
.get();
assertThat(response.getStatus()).isEqualTo(401);
}
}

View File

@ -1,6 +1,5 @@
package org.whispersystems.textsecuregcm.tests.push;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.ListenableFuture;
import com.relayrides.pushy.apns.ApnsClient;
import com.relayrides.pushy.apns.ApnsServerException;
@ -22,6 +21,7 @@ import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
import java.util.Date;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import io.netty.util.concurrent.DefaultEventExecutor;

View File

@ -1,6 +1,5 @@
package org.whispersystems.textsecuregcm.tests.push;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.SettableFuture;
import org.junit.Test;
import org.mockito.Matchers;
@ -15,6 +14,8 @@ import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
import java.util.Optional;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

View File

@ -6,6 +6,7 @@ import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.Device;
import java.util.HashSet;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertFalse;
@ -49,21 +50,21 @@ public class AccountTest {
Account recentAccount = new Account("+14152222222", new HashSet<Device>() {{
add(recentMasterDevice);
add(recentSecondaryDevice);
}});
}}, "1234".getBytes());
assertTrue(recentAccount.isActive());
Account oldSecondaryAccount = new Account("+14152222222", new HashSet<Device>() {{
add(recentMasterDevice);
add(agingSecondaryDevice);
}});
}}, "1234".getBytes());
assertTrue(oldSecondaryAccount.isActive());
Account agingPrimaryAccount = new Account("+14152222222", new HashSet<Device>() {{
add(oldMasterDevice);
add(agingSecondaryDevice);
}});
}}, "1234".getBytes());
assertTrue(agingPrimaryAccount.isActive());
}
@ -73,7 +74,7 @@ public class AccountTest {
Account oldPrimaryAccount = new Account("+14152222222", new HashSet<Device>() {{
add(oldMasterDevice);
add(oldSecondaryDevice);
}});
}}, "1234".getBytes());
assertFalse(oldPrimaryAccount.isActive());
}

View File

@ -1,6 +1,5 @@
package org.whispersystems.textsecuregcm.tests.storage;
import com.google.common.base.Optional;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
@ -18,17 +17,14 @@ import org.whispersystems.textsecuregcm.util.Util;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
public class DirectoryReconcilerTest {
@ -53,8 +49,6 @@ public class DirectoryReconcilerTest {
public void setup() {
when(account.getNumber()).thenReturn(VALID_NUMBER);
when(account.isActive()).thenReturn(true);
when(account.isVideoSupported()).thenReturn(true);
when(account.isVoiceSupported()).thenReturn(true);
when(inactiveAccount.getNumber()).thenReturn(INACTIVE_NUMBER);
when(inactiveAccount.isActive()).thenReturn(false);
@ -66,7 +60,7 @@ public class DirectoryReconcilerTest {
when(reconciliationClient.sendChunk(any())).thenReturn(successResponse);
when(reconciliationCache.getLastNumber()).thenReturn(Optional.absent());
when(reconciliationCache.getLastNumber()).thenReturn(Optional.empty());
when(reconciliationCache.claimActiveWork(any(), anyLong())).thenReturn(true);
when(reconciliationCache.isAccelerated()).thenReturn(false);
}
@ -155,7 +149,7 @@ public class DirectoryReconcilerTest {
assertThat(request.getValue().getNumbers()).isEqualTo(Collections.emptyList());
verify(reconciliationCache, times(1)).getLastNumber();
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.absent()));
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.empty()));
verify(reconciliationCache, times(1)).clearAccelerate();
verify(reconciliationCache, times(1)).isAccelerated();
verify(reconciliationCache, times(2)).claimActiveWork(any(), anyLong());
@ -192,7 +186,7 @@ public class DirectoryReconcilerTest {
assertThat(addedContact.getValue().getToken()).isEqualTo(Util.getContactToken(VALID_NUMBER));
verify(reconciliationCache, times(1)).getLastNumber();
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.absent()));
verify(reconciliationCache, times(1)).setLastNumber(eq(Optional.empty()));
verify(reconciliationCache, times(1)).clearAccelerate();
verify(reconciliationCache, times(1)).claimActiveWork(any(), anyLong());

View File

@ -1,21 +1,16 @@
package org.whispersystems.textsecuregcm.tests.util;
import com.google.common.base.Optional;
import org.whispersystems.dropwizard.simpleauth.AuthDynamicFeature;
import org.whispersystems.dropwizard.simpleauth.BasicCredentialAuthFilter;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
import org.whispersystems.textsecuregcm.auth.FederatedPeerAuthenticator;
import org.whispersystems.textsecuregcm.configuration.FederationConfiguration;
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.util.Base64;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import io.dropwizard.auth.AuthDynamicFeature;
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
@ -31,6 +26,8 @@ public class AuthHelper {
public static final String INVVALID_NUMBER = "+14151111111";
public static final String INVALID_PASSWORD = "bar";
public static final String VALID_IDENTITY = "BcxxDU9FGMda70E7+Uvm7pnQcEdXQ64aJCpPUeRSfcFo";
public static AccountsManager ACCOUNTS_MANAGER = mock(AccountsManager.class );
public static Account VALID_ACCOUNT = mock(Account.class );
public static Account VALID_ACCOUNT_TWO = mock(Account.class);
@ -53,29 +50,24 @@ public class AuthHelper {
when(VALID_ACCOUNT_TWO.getNumber()).thenReturn(VALID_NUMBER_TWO);
when(VALID_ACCOUNT.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE));
when(VALID_ACCOUNT_TWO.getAuthenticatedDevice()).thenReturn(Optional.of(VALID_DEVICE_TWO));
when(VALID_ACCOUNT.getRelay()).thenReturn(Optional.<String>absent());
when(VALID_ACCOUNT_TWO.getRelay()).thenReturn(Optional.<String>absent());
when(VALID_ACCOUNT.getRelay()).thenReturn(Optional.<String>empty());
when(VALID_ACCOUNT_TWO.getRelay()).thenReturn(Optional.<String>empty());
when(VALID_ACCOUNT.isActive()).thenReturn(true);
when(VALID_ACCOUNT_TWO.isActive()).thenReturn(true);
when(VALID_ACCOUNT.getIdentityKey()).thenReturn(VALID_IDENTITY);
when(ACCOUNTS_MANAGER.get(VALID_NUMBER)).thenReturn(Optional.of(VALID_ACCOUNT));
when(ACCOUNTS_MANAGER.get(VALID_NUMBER_TWO)).thenReturn(Optional.of(VALID_ACCOUNT_TWO));
List<FederatedPeer> peer = new LinkedList<FederatedPeer>() {{
add(new FederatedPeer("cyanogen", "https://foo", "foofoo", "bazzzzz"));
}};
FederationConfiguration federationConfiguration = mock(FederationConfiguration.class);
when(federationConfiguration.getPeers()).thenReturn(peer);
return new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Account>()
.setAuthenticator(new AccountAuthenticator(ACCOUNTS_MANAGER))
.setPrincipal(Account.class)
.buildAuthFilter(),
new BasicCredentialAuthFilter.Builder<FederatedPeer>()
.setAuthenticator(new FederatedPeerAuthenticator(federationConfiguration))
.setPrincipal(FederatedPeer.class)
.buildAuthFilter());
}
public static String getAuthHeader(String number, String password) {
return "Basic " + Base64.encodeBytes((number + ":" + password).getBytes());
}
public static String getUnidentifiedAccessHeader(byte[] key) {
return Base64.encodeBytes(key);
}
}

View File

@ -1,6 +1,5 @@
package org.whispersystems.textsecuregcm.tests.websocket;
import com.google.common.base.Optional;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
import org.eclipse.jetty.websocket.api.UpgradeRequest;
@ -27,6 +26,8 @@ import org.whispersystems.textsecuregcm.websocket.WebSocketAccountAuthenticator;
import org.whispersystems.textsecuregcm.websocket.WebSocketConnection;
import org.whispersystems.textsecuregcm.websocket.WebsocketAddress;
import org.whispersystems.websocket.WebSocketClient;
import org.whispersystems.websocket.auth.WebSocketAuthenticator;
import org.whispersystems.websocket.auth.WebSocketAuthenticator.AuthenticationResult;
import org.whispersystems.websocket.messages.WebSocketResponseMessage;
import org.whispersystems.websocket.session.WebSocketSessionContext;
@ -35,7 +36,9 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import io.dropwizard.auth.basic.BasicCredentials;
import static org.junit.Assert.*;
@ -72,7 +75,7 @@ public class WebSocketConnectionTest {
.thenReturn(Optional.of(account));
when(accountAuthenticator.authenticate(eq(new BasicCredentials(INVALID_USER, INVALID_PASSWORD))))
.thenReturn(Optional.<Account>absent());
.thenReturn(Optional.<Account>empty());
when(account.getAuthenticatedDevice()).thenReturn(Optional.of(device));
@ -81,8 +84,8 @@ public class WebSocketConnectionTest {
put("password", new LinkedList<String>() {{add(VALID_PASSWORD);}});
}});
Optional<Account> account = webSocketAuthenticator.authenticate(upgradeRequest);
when(sessionContext.getAuthenticated(Account.class)).thenReturn(account.get());
AuthenticationResult<Account> account = webSocketAuthenticator.authenticate(upgradeRequest);
when(sessionContext.getAuthenticated(Account.class)).thenReturn(account.getUser().orElse(null));
connectListener.onWebSocketConnect(sessionContext);
@ -94,7 +97,8 @@ public class WebSocketConnectionTest {
}});
account = webSocketAuthenticator.authenticate(upgradeRequest);
assertFalse(account.isPresent());
assertFalse(account.getUser().isPresent());
assertTrue(account.isRequired());
}
@Test
@ -125,7 +129,7 @@ public class WebSocketConnectionTest {
when(sender1.getDevices()).thenReturn(sender1devices);
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1));
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
when(accountsManager.get("sender2")).thenReturn(Optional.empty());
when(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
.thenReturn(outgoingMessagesList);
@ -160,7 +164,7 @@ public class WebSocketConnectionTest {
futures.get(2).setException(new IOException());
verify(storedMessages, times(1)).delete(eq(account.getNumber()), eq(2L), eq(2L), eq(false));
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender1"), eq(2222L), eq(Optional.<String>absent()));
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender1"), eq(2222L));
connection.onDispatchUnsubscribed(websocketAddress.serialize());
verify(client).close(anyInt(), anyString());
@ -208,7 +212,7 @@ public class WebSocketConnectionTest {
when(sender1.getDevices()).thenReturn(sender1devices);
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1));
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>empty());
when(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
.thenReturn(pendingMessagesList);
@ -250,7 +254,7 @@ public class WebSocketConnectionTest {
futures.get(1).set(response);
futures.get(0).setException(new IOException());
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()), eq(Optional.<String>absent()));
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()));
verify(websocketSender, times(1)).queueMessage(eq(account), eq(device), any(Envelope.class));
verify(pushSender, times(1)).sendQueuedNotification(eq(account), eq(device));
@ -285,14 +289,14 @@ public class WebSocketConnectionTest {
.build();
List<OutgoingMessageEntity> pendingMessages = new LinkedList<OutgoingMessageEntity>() {{
add(new OutgoingMessageEntity(1, true, firstMessage.getType().getNumber(), firstMessage.getRelay(),
add(new OutgoingMessageEntity(1, true, UUID.randomUUID(), firstMessage.getType().getNumber(), firstMessage.getRelay(),
firstMessage.getTimestamp(), firstMessage.getSource(),
firstMessage.getSourceDevice(), firstMessage.getLegacyMessage().toByteArray(),
firstMessage.getContent().toByteArray()));
add(new OutgoingMessageEntity(2, false, secondMessage.getType().getNumber(), secondMessage.getRelay(),
firstMessage.getContent().toByteArray(), 0));
add(new OutgoingMessageEntity(2, false, UUID.randomUUID(), secondMessage.getType().getNumber(), secondMessage.getRelay(),
secondMessage.getTimestamp(), secondMessage.getSource(),
secondMessage.getSourceDevice(), secondMessage.getLegacyMessage().toByteArray(),
secondMessage.getContent().toByteArray()));
secondMessage.getContent().toByteArray(), 0));
}};
OutgoingMessageEntityList pendingMessagesList = new OutgoingMessageEntityList(pendingMessages, false);
@ -313,7 +317,7 @@ public class WebSocketConnectionTest {
when(sender1.getDevices()).thenReturn(sender1devices);
when(accountsManager.get("sender1")).thenReturn(Optional.of(sender1));
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>absent());
when(accountsManager.get("sender2")).thenReturn(Optional.<Account>empty());
when(storedMessages.getMessagesForDevice(account.getNumber(), device.getId()))
.thenReturn(pendingMessagesList);
@ -346,7 +350,7 @@ public class WebSocketConnectionTest {
futures.get(1).set(response);
futures.get(0).setException(new IOException());
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()), eq(Optional.<String>absent()));
verify(receiptSender, times(1)).sendReceipt(eq(account), eq("sender2"), eq(secondMessage.getTimestamp()));
verifyNoMoreInteractions(websocketSender);
verifyNoMoreInteractions(pushSender);
@ -356,8 +360,8 @@ public class WebSocketConnectionTest {
private OutgoingMessageEntity createMessage(long id, boolean cached, String sender, long timestamp, boolean receipt, String content) {
return new OutgoingMessageEntity(id, cached, receipt ? Envelope.Type.RECEIPT_VALUE : Envelope.Type.CIPHERTEXT_VALUE,
null, timestamp, sender, 1, content.getBytes(), null);
return new OutgoingMessageEntity(id, cached, UUID.randomUUID(), receipt ? Envelope.Type.RECEIPT_VALUE : Envelope.Type.CIPHERTEXT_VALUE,
null, timestamp, sender, 1, content.getBytes(), null, 0);
}
}