diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3861d5c6..7753d0dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,8 @@
# pusher-websocket-java changelog
-This Changelog is no longer being updated. For any further changes please see the Releases section on this Github repository - https://github.com/pusher/pusher-websocket-java/releases
+## Version 2.1.0 - 8th April 2020
+
+* Added support for [private encrypted channels](https://pusher.com/docs/channels/using_channels/encrypted-channels)
## Version 2.0.2
diff --git a/README.md b/README.md
index 4bba7c46..83aaf147 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
[](https://travis-ci.org/pusher/pusher-websocket-java)
[](https://codecov.io/gh/pusher/pusher-websocket-java)
+[](https://search.maven.org/search?q=g:%22com.pusher%22%20AND%20a:%22pusher-java-client%22)
Pusher Channels client library for Java targeting **Android** and general Java.
@@ -30,6 +31,7 @@ This README covers the following topics:
- [Subscribing to channels](#subscribing-to-channels)
- [Public channels](#public-channels)
- [Private channels](#private-channels)
+ - [Private encrypted channels [BETA]](#private-encrypted-channels)
- [Presence channels](#presence-channels)
- [The User object](#the-user-object)
- [Binding and handling events](#binding-and-handling-events)
@@ -59,7 +61,7 @@ The pusher-java-client is available in Maven Central.
com.pusher
pusher-java-client
- 2.0.2
+ 2.1.0
```
@@ -68,7 +70,7 @@ The pusher-java-client is available in Maven Central.
```groovy
dependencies {
- compile 'com.pusher:pusher-java-client:2.0.2'
+ compile 'com.pusher:pusher-java-client:2.1.0'
}
```
@@ -271,6 +273,37 @@ PrivateChannel channel = pusher.subscribePrivate("private-channel",
});
```
+### Private encrypted channels [BETA]
+
+Similar to Private channels, you can also subscribe to a
+[private encrypted channel](https://pusher.com/docs/channels/using_channels/encrypted-channels).
+This library now fully supports end-to-end encryption. This means that only you and your connected clients will be able to read your messages. Pusher cannot decrypt them.
+
+Like the private channel, you must provide your own authentication endpoint,
+with your own encryption master key. There is a
+[demonstration endpoint to look at using nodejs](https://github.com/pusher/pusher-channels-auth-example#using-e2e-encryption).
+
+To get started you need to subscribe to your channel, provide a `PrivateEncryptedChannelEventListener`, and a list of the events you are
+interested in, for example:
+
+```java
+PrivateEncryptedChannel privateEncryptedChannel =
+ pusher.subscribePrivateEncrypted("private-encrypted-channel", listener, "my-event");
+```
+
+In addition to the events that are possible on public channels the
+`PrivateEncryptedChannelEventListener` also has the following methods:
+* `onAuthenticationFailure(String message, Exception e)` - This is called if
+the `Authorizer` does not successfully authenticate the subscription:
+* `onDecryptionFailure(String event, String reason);` - This is called if the message cannot be
+decrypted. The decryption will attempt to refresh the shared secret key once
+from the `Authorizer`.
+
+There is a
+[working example in the repo](https://github.com/pusher/pusher-websocket-java/blob/master/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java)
+which you can use with the
+[demonstration authorization endpoint](https://github.com/pusher/pusher-channels-auth-example#using-e2e-encryption)
+
### Presence channels
[Presence channels](https://pusher.com/docs/channels/using_channels/presence-channels) are private channels which provide additional events exposing who is currently subscribed to the channel. Since they extend private channels they also need to be authenticated (see [authenticating channel subscriptions](https://pusher.com/docs/channels/server_api/authenticating-users)).
diff --git a/build.gradle b/build.gradle
index bf976aa7..fb8a8815 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,7 +22,7 @@ apply plugin: 'signing'
apply plugin: 'jacoco'
group = "com.pusher"
-version = "2.0.2"
+version = "2.1.0"
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
@@ -45,9 +45,12 @@ repositories {
dependencies {
compile "com.google.code.gson:gson:2.2.2"
compile "org.java-websocket:Java-WebSocket:1.4.0"
+
testCompile "org.mockito:mockito-all:1.8.5"
testCompile "org.powermock:powermock-module-junit4:1.4.11"
testCompile "org.powermock:powermock-api-mockito:1.4.11"
+
+ testImplementation "com.google.truth:truth:1.0.1"
}
@@ -64,11 +67,13 @@ javadoc {
options.overview = file("src/main/javadoc/overview.html")
// uncomment this to use the custom javadoc styles
//options.stylesheetFile = file("src/main/javadoc/css/styles.css")
- exclude "**/com/pusher/client/channel/impl/*"
- exclude "**/com/pusher/client/connection/impl/*"
- exclude "**/com/pusher/client/connection/websocket/*"
- exclude "**/org/java_websocket/*"
- exclude "**/com/pusher/client/example/*"
+ exclude "com/pusher/client/channel/impl/*"
+ exclude "com/pusher/client/connection/impl/*"
+ exclude "com/pusher/client/connection/websocket/*"
+ exclude "com/pusher/client/crypto/nacl/*"
+ exclude "com/pusher/client/util/internal/*"
+ exclude "org/java_websocket/*"
+ exclude "com/pusher/client/example/*"
options.linkSource = true
}
diff --git a/src/main/java/com/pusher/client/Pusher.java b/src/main/java/com/pusher/client/Pusher.java
index b0dfec03..34746c8a 100644
--- a/src/main/java/com/pusher/client/Pusher.java
+++ b/src/main/java/com/pusher/client/Pusher.java
@@ -2,6 +2,8 @@
import com.pusher.client.channel.Channel;
import com.pusher.client.channel.ChannelEventListener;
+import com.pusher.client.channel.PrivateEncryptedChannel;
+import com.pusher.client.channel.PrivateEncryptedChannelEventListener;
import com.pusher.client.channel.PresenceChannel;
import com.pusher.client.channel.PresenceChannelEventListener;
import com.pusher.client.channel.PrivateChannel;
@@ -11,6 +13,7 @@
import com.pusher.client.channel.impl.InternalChannel;
import com.pusher.client.channel.impl.PresenceChannelImpl;
import com.pusher.client.channel.impl.PrivateChannelImpl;
+import com.pusher.client.channel.impl.PrivateEncryptedChannelImpl;
import com.pusher.client.connection.Connection;
import com.pusher.client.connection.ConnectionEventListener;
import com.pusher.client.connection.ConnectionState;
@@ -284,6 +287,38 @@ public PrivateChannel subscribePrivate(final String channelName, final PrivateCh
return channel;
}
+
+ /**
+ * Subscribes to a {@link com.pusher.client.channel.PrivateEncryptedChannel} which
+ * requires authentication.
+ *
+ * @param channelName The name of the channel to subscribe to.
+ * @param listener A listener to be informed of both Pusher channel protocol events and
+ * subscription data events.
+ * @param eventNames An optional list of names of events to be bound to on the channel.
+ * The equivalent of calling
+ * {@link com.pusher.client.channel.Channel#bind(String, SubscriptionEventListener)}
+ * one or more times.
+ * @return A new {@link com.pusher.client.channel.PrivateEncryptedChannel} representing
+ * the subscription.
+ * @throws IllegalStateException if a {@link com.pusher.client.Authorizer} has not been set for
+ * the {@link Pusher} instance via {@link #Pusher(String, PusherOptions)}.
+ */
+ public PrivateEncryptedChannel subscribePrivateEncrypted(
+ final String channelName,
+ final PrivateEncryptedChannelEventListener listener,
+ final String... eventNames) {
+
+ throwExceptionIfNoAuthorizerHasBeenSet();
+
+ final PrivateEncryptedChannelImpl channel = factory.newPrivateEncryptedChannel(
+ connection, channelName, pusherOptions.getAuthorizer());
+ channelManager.subscribeTo(channel, listener, eventNames);
+
+ return channel;
+ }
+
+
/**
* Subscribes to a {@link com.pusher.client.channel.PresenceChannel} which
* requires authentication.
@@ -363,6 +398,16 @@ public PrivateChannel getPrivateChannel(String channelName){
return channelManager.getPrivateChannel(channelName);
}
+ /**
+ *
+ * @param channelName The name of the private encrypted channel to be retrieved
+ * @return A private encrypted channel, or null if it could not be found
+ * @throws IllegalArgumentException if you try to retrieve a public or presence channel.
+ */
+ public PrivateEncryptedChannel getPrivateEncryptedChannel(String channelName){
+ return channelManager.getPrivateEncryptedChannel(channelName);
+ }
+
/**
*
* @param channelName The name of the presence channel to be retrieved
diff --git a/src/main/java/com/pusher/client/channel/PrivateChannelEventListener.java b/src/main/java/com/pusher/client/channel/PrivateChannelEventListener.java
index 9a2da678..7bbc7d0b 100644
--- a/src/main/java/com/pusher/client/channel/PrivateChannelEventListener.java
+++ b/src/main/java/com/pusher/client/channel/PrivateChannelEventListener.java
@@ -7,10 +7,8 @@ public interface PrivateChannelEventListener extends ChannelEventListener {
/**
* Called when an attempt to authenticate a private channel fails.
*
- * @param message
- * A description of the problem.
- * @param e
- * An associated exception, if available.
+ * @param message A description of the problem.
+ * @param e An associated exception, if available.
*/
void onAuthenticationFailure(String message, Exception e);
}
diff --git a/src/main/java/com/pusher/client/channel/PrivateEncryptedChannel.java b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannel.java
new file mode 100644
index 00000000..98f29dfe
--- /dev/null
+++ b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannel.java
@@ -0,0 +1,9 @@
+package com.pusher.client.channel;
+
+/**
+ * Represents a subscription to an encrypted private channel.
+ */
+public interface PrivateEncryptedChannel extends Channel {
+
+ // it's not currently possible to send a message using private encrypted channels
+}
diff --git a/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java
new file mode 100644
index 00000000..405f764c
--- /dev/null
+++ b/src/main/java/com/pusher/client/channel/PrivateEncryptedChannelEventListener.java
@@ -0,0 +1,12 @@
+package com.pusher.client.channel;
+
+/**
+ * Interface to listen to private encrypted channel events.
+ * Note: This needs to extend the PrivateChannelEventListener because in the
+ * ChannelManager handleAuthenticationFailure we assume it's safe to cast to a
+ * PrivateChannelEventListener
+ */
+public interface PrivateEncryptedChannelEventListener extends PrivateChannelEventListener {
+
+ void onDecryptionFailure(String event, String reason);
+}
diff --git a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java
index 39783c4b..b6f41309 100644
--- a/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java
+++ b/src/main/java/com/pusher/client/channel/impl/ChannelImpl.java
@@ -13,7 +13,7 @@
import com.pusher.client.util.Factory;
public class ChannelImpl implements InternalChannel {
- private final Gson GSON;
+ protected final Gson GSON;
private static final String INTERNAL_EVENT_PREFIX = "pusher_internal:";
protected static final String SUBSCRIPTION_SUCCESS_EVENT = "pusher_internal:subscription_succeeded";
protected final String name;
@@ -89,33 +89,29 @@ public boolean isSubscribed() {
/* InternalChannel implementation */
+ @Override
+ public PusherEvent prepareEvent(String event, String message) {
+ return GSON.fromJson(message, PusherEvent.class);
+ }
+
@Override
public void onMessage(final String event, final String message) {
if (event.equals(SUBSCRIPTION_SUCCESS_EVENT)) {
updateState(ChannelState.SUBSCRIBED);
- }
- else {
- final Set listeners;
- synchronized (lock) {
- final Set sharedListeners = eventNameToListenerMap.get(event);
- if (sharedListeners != null) {
- listeners = new HashSet(sharedListeners);
- }
- else {
- listeners = null;
- }
- }
-
+ } else {
+ final Set listeners = getInterestedListeners(event);
if (listeners != null) {
- for (final SubscriptionEventListener listener : listeners) {
- final PusherEvent e = GSON.fromJson(message, PusherEvent.class);
- factory.queueOnEventThread(new Runnable() {
- @Override
- public void run() {
- listener.onEvent(e);
- }
- });
+ final PusherEvent pusherEvent = prepareEvent(event, message);
+ if (pusherEvent != null) {
+ for (final SubscriptionEventListener listener : listeners) {
+ factory.queueOnEventThread(new Runnable() {
+ @Override
+ public void run() {
+ listener.onEvent(pusherEvent);
+ }
+ });
+ }
}
}
}
@@ -213,4 +209,18 @@ private void validateArguments(final String eventName, final SubscriptionEventLi
"Cannot bind or unbind to events on a channel that has been unsubscribed. Call Pusher.subscribe() to resubscribe to this channel");
}
}
+
+ protected Set getInterestedListeners(String event) {
+ synchronized (lock) {
+
+ final Set sharedListeners =
+ eventNameToListenerMap.get(event);
+
+ if (sharedListeners == null) {
+ return null;
+ }
+
+ return new HashSet<>(sharedListeners);
+ }
+ }
}
diff --git a/src/main/java/com/pusher/client/channel/impl/ChannelManager.java b/src/main/java/com/pusher/client/channel/impl/ChannelManager.java
index 08a019ae..bc6cf268 100644
--- a/src/main/java/com/pusher/client/channel/impl/ChannelManager.java
+++ b/src/main/java/com/pusher/client/channel/impl/ChannelManager.java
@@ -8,6 +8,7 @@
import com.pusher.client.channel.Channel;
import com.pusher.client.channel.ChannelEventListener;
import com.pusher.client.channel.ChannelState;
+import com.pusher.client.channel.PrivateEncryptedChannel;
import com.pusher.client.channel.PresenceChannel;
import com.pusher.client.channel.PrivateChannel;
import com.pusher.client.channel.PrivateChannelEventListener;
@@ -46,6 +47,14 @@ public PrivateChannel getPrivateChannel(String channelName) throws IllegalArgume
}
}
+ public PrivateEncryptedChannel getPrivateEncryptedChannel(String channelName) throws IllegalArgumentException{
+ if (!channelName.startsWith("private-encrypted-")) {
+ throw new IllegalArgumentException("Encrypted private channels must begin with 'private-encrypted-'");
+ } else {
+ return (PrivateEncryptedChannel) findChannelInChannelMap(channelName);
+ }
+ }
+
public PresenceChannel getPresenceChannel(String channelName) throws IllegalArgumentException{
if (!channelName.startsWith("presence-")) {
throw new IllegalArgumentException("Presence channels must begin with 'presence-'");
@@ -141,7 +150,7 @@ public void run() {
connection.sendMessage(message);
channel.updateState(ChannelState.SUBSCRIBE_SENT);
} catch (final AuthorizationFailureException e) {
- clearDownSubscription(channel, e);
+ handleAuthenticationFailure(channel, e);
}
}
}
@@ -158,7 +167,7 @@ public void run() {
});
}
- private void clearDownSubscription(final InternalChannel channel, final Exception e) {
+ private void handleAuthenticationFailure(final InternalChannel channel, final Exception e) {
channelNameToChannelMap.remove(channel.getName());
channel.updateState(ChannelState.FAILED);
diff --git a/src/main/java/com/pusher/client/channel/impl/InternalChannel.java b/src/main/java/com/pusher/client/channel/impl/InternalChannel.java
index e1668b7a..b6d8e032 100644
--- a/src/main/java/com/pusher/client/channel/impl/InternalChannel.java
+++ b/src/main/java/com/pusher/client/channel/impl/InternalChannel.java
@@ -3,6 +3,7 @@
import com.pusher.client.channel.Channel;
import com.pusher.client.channel.ChannelEventListener;
import com.pusher.client.channel.ChannelState;
+import com.pusher.client.channel.PusherEvent;
public interface InternalChannel extends Channel, Comparable {
@@ -10,6 +11,8 @@ public interface InternalChannel extends Channel, Comparable {
String toUnsubscribeMessage();
+ PusherEvent prepareEvent(String event, String message);
+
void onMessage(String event, String message);
void updateState(ChannelState state);
diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateChannelImpl.java
index 5260fdf6..e6c27a54 100644
--- a/src/main/java/com/pusher/client/channel/impl/PrivateChannelImpl.java
+++ b/src/main/java/com/pusher/client/channel/impl/PrivateChannelImpl.java
@@ -116,7 +116,10 @@ public String toSubscribeMessage() {
@Override
protected String[] getDisallowedNameExpressions() {
- return new String[] { "^(?!private-).*" };
+ return new String[] {
+ "^(?!private-).*", // double negative, don't not start with private-
+ "^private-encrypted-.*" // doesn't start with private-encrypted-
+ };
}
/**
diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java
new file mode 100644
index 00000000..66a9c329
--- /dev/null
+++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java
@@ -0,0 +1,220 @@
+package com.pusher.client.channel.impl;
+
+import com.pusher.client.AuthorizationFailureException;
+import com.pusher.client.Authorizer;
+import com.pusher.client.channel.ChannelState;
+import com.pusher.client.channel.PrivateEncryptedChannel;
+import com.pusher.client.channel.PrivateEncryptedChannelEventListener;
+import com.pusher.client.channel.PusherEvent;
+import com.pusher.client.channel.SubscriptionEventListener;
+import com.pusher.client.connection.ConnectionEventListener;
+import com.pusher.client.connection.ConnectionState;
+import com.pusher.client.connection.ConnectionStateChange;
+import com.pusher.client.connection.impl.InternalConnection;
+import com.pusher.client.crypto.nacl.AuthenticityException;
+import com.pusher.client.crypto.nacl.SecretBoxOpener;
+import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory;
+import com.pusher.client.util.Factory;
+import com.pusher.client.util.internal.Base64;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class PrivateEncryptedChannelImpl extends ChannelImpl implements PrivateEncryptedChannel {
+
+ private final InternalConnection connection;
+ private final Authorizer authorizer;
+ private SecretBoxOpenerFactory secretBoxOpenerFactory;
+ private SecretBoxOpener secretBoxOpener;
+
+ // For not hanging on to shared secret past the Pusher.disconnect() call,
+ // i.e. when not necessary. Pusher.connect(...) call will trigger re-subscribe
+ // and hence re-authenticate which creates a new secretBoxOpener.
+ private ConnectionEventListener disposeSecretBoxOpenerOnDisconnectedListener =
+ new ConnectionEventListener() {
+
+ @Override
+ public void onConnectionStateChange(ConnectionStateChange change) {
+ disposeSecretBoxOpener();
+ }
+
+ @Override
+ public void onError(String message, String code, Exception e) {
+ // nop
+ }
+ };
+
+ public PrivateEncryptedChannelImpl(final InternalConnection connection,
+ final String channelName,
+ final Authorizer authorizer,
+ final Factory factory,
+ final SecretBoxOpenerFactory secretBoxOpenerFactory) {
+ super(channelName, factory);
+ this.connection = connection;
+ this.authorizer = authorizer;
+ this.secretBoxOpenerFactory = secretBoxOpenerFactory;
+ }
+
+ @Override
+ public void bind(final String eventName, final SubscriptionEventListener listener) {
+
+ if (!(listener instanceof PrivateEncryptedChannelEventListener)) {
+ throw new IllegalArgumentException(
+ "Only instances of PrivateEncryptedChannelEventListener can be bound " +
+ "to a private encrypted channel");
+ }
+
+ super.bind(eventName, listener);
+ }
+
+ @Override
+ public String toSubscribeMessage() {
+ String authKey = authenticate();
+
+ // create the data part
+ final Map dataMap = new LinkedHashMap<>();
+ dataMap.put("channel", name);
+ dataMap.put("auth", authKey);
+
+ // create the wrapper part
+ final Map jsonObject = new LinkedHashMap<>();
+ jsonObject.put("event", "pusher:subscribe");
+ jsonObject.put("data", dataMap);
+
+ return GSON.toJson(jsonObject);
+ }
+
+ private String authenticate() {
+ try {
+ @SuppressWarnings("rawtypes") // anything goes in JS
+ final Map authResponse = GSON.fromJson(getAuthResponse(), Map.class);
+
+ final String auth = (String) authResponse.get("auth");
+ final String sharedSecret = (String) authResponse.get("shared_secret");
+
+ if (auth == null || sharedSecret == null) {
+ throw new AuthorizationFailureException("Didn't receive all the fields expected " +
+ "from the Authorizer, expected an auth and shared_secret.");
+ } else {
+ createSecretBoxOpener(Base64.decode(sharedSecret));
+ return auth;
+ }
+ } catch (final AuthorizationFailureException e) {
+ throw e; // pass this upwards
+ } catch (final Exception e) {
+ // any other errors need to be captured properly and passed upwards
+ throw new AuthorizationFailureException("Unable to parse response from Authorizer", e);
+ }
+ }
+
+ private void createSecretBoxOpener(byte[] key) {
+ secretBoxOpener = secretBoxOpenerFactory.create(key);
+ setListenerToDisposeSecretBoxOpenerOnDisconnected();
+ }
+
+ private void setListenerToDisposeSecretBoxOpenerOnDisconnected() {
+ connection.bind(ConnectionState.DISCONNECTED,
+ disposeSecretBoxOpenerOnDisconnectedListener);
+ }
+
+ @Override
+ public void updateState(ChannelState state) {
+ super.updateState(state);
+
+ if (state == ChannelState.UNSUBSCRIBED) {
+ disposeSecretBoxOpener();
+ }
+ }
+
+ @Override
+ public PusherEvent prepareEvent(String event, String message) {
+
+ try {
+ return decryptMessage(message);
+ } catch (AuthenticityException e1) {
+
+ // retry once only.
+ disposeSecretBoxOpener();
+ authenticate();
+
+ try {
+ return decryptMessage(message);
+ } catch (AuthenticityException e2) {
+ // deliberately not destroying the secretBoxOpener so the next message
+ // has an opportunity to fetch a new key and decrypt
+ notifyListenersOfDecryptFailure(event, "Failed to decrypt message.");
+ }
+ }
+
+ return null;
+ }
+
+ private void notifyListenersOfDecryptFailure(final String event, final String reason) {
+ Set listeners = getInterestedListeners(event);
+ if (listeners != null) {
+ for (SubscriptionEventListener listener : listeners) {
+ ((PrivateEncryptedChannelEventListener)listener).onDecryptionFailure(
+ event, reason);
+ }
+ }
+ }
+
+ private class EncryptedReceivedData {
+ String nonce;
+ String ciphertext;
+
+ public byte[] getNonce() {
+ return Base64.decode(nonce);
+ }
+
+ public byte[] getCiphertext() {
+ return Base64.decode(ciphertext);
+ }
+ }
+
+ private PusherEvent decryptMessage(String message) {
+
+ Map receivedMessage =
+ GSON.>fromJson(message, Map.class);
+
+ final EncryptedReceivedData encryptedReceivedData =
+ GSON.fromJson((String)receivedMessage.get("data"), EncryptedReceivedData.class);
+
+ String decryptedData = new String(secretBoxOpener.open(
+ encryptedReceivedData.getCiphertext(),
+ encryptedReceivedData.getNonce()));
+
+ receivedMessage.replace("data", decryptedData);
+
+ return new PusherEvent(receivedMessage);
+ }
+
+ private void disposeSecretBoxOpener() {
+ if (secretBoxOpener != null) {
+ secretBoxOpener.clearKey();
+ secretBoxOpener = null;
+ removeListenerToDisposeSecretBoxOpenerOnDisconnected();
+ }
+ }
+
+ private void removeListenerToDisposeSecretBoxOpenerOnDisconnected() {
+ connection.unbind(ConnectionState.DISCONNECTED,
+ disposeSecretBoxOpenerOnDisconnectedListener);
+ }
+
+ private String getAuthResponse() {
+ final String socketId = connection.getSocketId();
+ return authorizer.authorize(getName(), socketId);
+ }
+
+ @Override
+ protected String[] getDisallowedNameExpressions() {
+ return new String[] { "^(?!private-encrypted-).*" };
+ }
+
+ @Override
+ public String toString() {
+ return String.format("[Private Encrypted Channel: name=%s]", name);
+ }
+}
diff --git a/src/main/java/com/pusher/client/crypto/nacl/AuthenticityException.java b/src/main/java/com/pusher/client/crypto/nacl/AuthenticityException.java
new file mode 100644
index 00000000..18150119
--- /dev/null
+++ b/src/main/java/com/pusher/client/crypto/nacl/AuthenticityException.java
@@ -0,0 +1,27 @@
+/*
+Copyright 2015 Eve Freeman
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+*/
+
+package com.pusher.client.crypto.nacl;
+
+public class AuthenticityException extends RuntimeException {
+}
diff --git a/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java
new file mode 100644
index 00000000..e77a0574
--- /dev/null
+++ b/src/main/java/com/pusher/client/crypto/nacl/Poly1305.java
@@ -0,0 +1,1557 @@
+/*
+Copyright 2015 Eve Freeman
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+*/
+
+package com.pusher.client.crypto.nacl;
+
+public class Poly1305 {
+ public static final int TAG_SIZE = 16;
+
+ private static final double ALPHAM_80 = 0.00000000558793544769287109375d;
+ private static final double ALPHAM_48 = 24.0d;
+ private static final double ALPHAM_16 = 103079215104.0d;
+ private static final double ALPHA_0 = 6755399441055744.0d;
+ private static final double ALPHA_18 = 1770887431076116955136.0d;
+ private static final double ALPHA_32 = 29014219670751100192948224.0d;
+ private static final double ALPHA_50 = 7605903601369376408980219232256.0d;
+ private static final double ALPHA_64 = 124615124604835863084731911901282304.0d;
+ private static final double ALPHA_82 = 32667107224410092492483962313449748299776.0d;
+ private static final double ALPHA_96 = 535217884764734955396857238543560676143529984.0d;
+ private static final double ALPHA_112 = 35076039295941670036888435985190792471742381031424.0d;
+ private static final double ALPHA_130 = 9194973245195333150150082162901855101712434733101613056.0d;
+ private static final double SCALE = 0.0000000000000000000000000000000000000036734198463196484624023016788195177431833298649127735047148490821200539357960224151611328125d;
+ private static final double OFFSET_0 = 6755408030990331.0d;
+ private static final double OFFSET_1 = 29014256564239239022116864.0d;
+ private static final double OFFSET_2 = 124615283061160854719918951570079744.0d;
+ private static final double OFFSET_3 = 535219245894202480694386063513315216128475136.0d;
+
+ private static long uint32(long x) {
+ return 0xFFFFFFFF & x;
+ }
+
+ private static double longBitsToDouble(long bits) {
+ int s = ((bits >> 63) == 0) ? 1 : -1;
+ int e = (int) ((bits >> 52) & 0x7ffL);
+ long m = (e == 0) ?
+ (bits & 0xfffffffffffffL) << 1 :
+ (bits & 0xfffffffffffffL) | 0x10000000000000L;
+ return (double) s * (double) m * Math.pow(2, e - 1075);
+ }
+
+ public static boolean verify(byte[] mac, byte[] m, byte[] key) {
+ byte[] tmp = sum(m, key);
+ //Util.printHex("tmp", tmp);
+ //Util.printHex("mac", mac);
+ return Subtle.constantTimeCompare(tmp, mac);
+ }
+
+ // Sum generates an authenticator for m using a one-time key and puts the
+ // 16-byte result into out. Authenticating two different messages with the same
+ // key allows an attacker to forge messages at will.
+ @SuppressWarnings("SuspiciousNameCombination")
+ public static byte[] sum(byte[] m, byte[] key) {
+ byte[] r = key.clone();
+ byte[] s = new byte[16];
+ System.arraycopy(key, 16, s, 0, s.length);
+
+ double y7;
+ double y6;
+ double y1;
+ double y0;
+ double y5;
+ double y4;
+ double x7;
+ double x6;
+ double x1;
+ double x0;
+ double y3;
+ double y2;
+ double x5;
+ double r3lowx0;
+ double x4;
+ double r0lowx6;
+ double x3;
+ double r3highx0;
+ double x2;
+ double r0highx6;
+ double r0lowx0;
+ double sr1lowx6;
+ double r0highx0;
+ double sr1highx6;
+ double sr3low;
+ double r1lowx0;
+ double sr2lowx6;
+ double r1highx0;
+ double sr2highx6;
+ double r2lowx0;
+ double sr3lowx6;
+ double r2highx0;
+ double sr3highx6;
+ double r1highx4;
+ double r1lowx4;
+ double r0highx4;
+ double r0lowx4;
+ double sr3highx4;
+ double sr3lowx4;
+ double sr2highx4;
+ double sr2lowx4;
+ double r0lowx2;
+ double r0highx2;
+ double r1lowx2;
+ double r1highx2;
+ double r2lowx2;
+ double r2highx2;
+ double sr3lowx2;
+ double sr3highx2;
+ double z0;
+ double z1;
+ double z2;
+ double z3;
+ long m0;
+ long m1;
+ long m2;
+ long m3;
+ long m00;//uint32
+ long m01;//uint32
+ long m02;//uint32
+ long m03;//uint32
+ long m10;//uint32
+ long m11;//uint32
+ long m12;//uint32
+ long m13;//uint32
+ long m20;//uint32
+ long m21;//uint32
+ long m22;//uint32
+ long m23;//uint32
+ long m30;//uint32
+ long m31;//uint32
+ long m32;//uint32
+ long m33;//uint64
+ long lbelow2;//int32
+ long lbelow3;//int32
+ long lbelow4;//int32
+ long lbelow5;//int32
+ long lbelow6;//int32
+ long lbelow7;//int32
+ long lbelow8;//int32
+ long lbelow9;//int32
+ long lbelow10;//int32
+ long lbelow11;//int32
+ long lbelow12;//int32
+ long lbelow13;//int32
+ long lbelow14;//int32
+ long lbelow15;//int32
+ long s00;//uint32
+ long s01;//uint32
+ long s02;//uint32
+ long s03;//uint32
+ long s10;//uint32
+ long s11;//uint32
+ long s12;//uint32
+ long s13;//uint32
+ long s20;//uint32
+ long s21;//uint32
+ long s22;//uint32
+ long s23;//uint32
+ long s30;//uint32
+ long s31;//uint32
+ long s32;//uint32
+ long s33;//uint32
+ long bits32;//uint64
+ long f;//uint64
+ long f0;//uint64
+ long f1;//uint64
+ long f2;//uint64
+ long f3;//uint64
+ long f4;//uint64
+ long g;//uint64
+ long g0;//uint64
+ long g1;//uint64
+ long g2;//uint64
+ long g3;//uint64
+ long g4;//uint64
+
+ long p = 0;
+
+ int l = m.length;
+
+ long r00 = 0xFF & r[0];
+ long r01 = 0xFF & r[1];
+ long r02 = 0xFF & r[2];
+ long r0 = 2151;
+
+ long r03 = 0xFF & r[3];
+ r03 &= 15;
+ r0 <<= 51;
+
+ long r10 = 0xFF & r[4];
+ r10 &= 252;
+ r01 <<= 8;
+ r0 += r00;
+
+ long r11 = 0xFF & r[5];
+ r02 <<= 16;
+ r0 += r01;
+
+ long r12 = 0xFF & r[6];
+ r03 <<= 24;
+ r0 += r02;
+
+ long r13 = 0xFF & r[7];
+ r13 &= 15;
+ long r1 = 2215;
+ r0 += r03;
+
+ long d0 = r0;
+ r1 <<= 51;
+ long r2 = 2279;
+
+ long r20 = 0xFF & r[8];
+ r20 &= 252;
+ r11 <<= 8;
+ r1 += r10;
+
+ long r21 = 0xFF & r[9];
+ r12 <<= 16;
+ r1 += r11;
+
+ long r22 = 0xFF & r[10];
+ r13 <<= 24;
+ r1 += r12;
+
+ long r23 = 0xFF & r[11];
+ r23 &= 15;
+ r2 <<= 51;
+ r1 += r13;
+
+ long d1 = r1;
+ r21 <<= 8;
+ r2 += r20;
+
+ long r30 = 0xFF & r[12];
+ r30 &= 252;
+ r22 <<= 16;
+ r2 += r21;
+
+ long r31 = 0xFF & r[13];
+ r23 <<= 24;
+ r2 += r22;
+
+ long r32 = 0xFF & r[14];
+ r2 += r23;
+ long r3 = 2343;
+
+ long d2 = r2;
+ r3 <<= 51;
+
+ long r33 = 0xFF & r[15];
+ r33 &= 15;
+ r31 <<= 8;
+ r3 += r30;
+
+ r32 <<= 16;
+ r3 += r31;
+
+ r33 <<= 24;
+ r3 += r32;
+
+ r3 += r33;
+ double h0 = ALPHA_32 - ALPHA_32;
+
+ long d3 = r3;
+ double h1 = ALPHA_32 - ALPHA_32;
+
+ double h2 = ALPHA_32 - ALPHA_32;
+
+ double h3 = ALPHA_32 - ALPHA_32;
+
+ double h4 = ALPHA_32 - ALPHA_32;
+
+ double r0low = Double.longBitsToDouble(d0);
+ double h5 = ALPHA_32 - ALPHA_32;
+
+ double r1low = longBitsToDouble(d1);
+ double h6 = ALPHA_32 - ALPHA_32;
+
+ double r2low = Double.longBitsToDouble(d2);
+ double h7 = ALPHA_32 - ALPHA_32;
+
+ r0low -= ALPHA_0;
+
+ r1low -= ALPHA_32;
+
+ r2low -= ALPHA_64;
+
+ double r0high = r0low + ALPHA_18;
+
+ double r3low = Double.longBitsToDouble(d3);
+
+ double r1high = r1low + ALPHA_50;
+ double sr1low = SCALE * r1low;
+
+ double r2high = r2low + ALPHA_82;
+ double sr2low = SCALE * r2low;
+
+ r0high -= ALPHA_18;
+ double r0high_stack = r0high;
+
+ r3low -= ALPHA_96;
+
+ r1high -= ALPHA_50;
+ double r1high_stack = r1high;
+
+ double sr1high = sr1low + ALPHAM_80;
+
+ r0low -= r0high;
+
+ r2high -= ALPHA_82;
+ sr3low = SCALE * r3low;
+
+ double sr2high = sr2low + ALPHAM_48;
+
+ r1low -= r1high;
+ double r1low_stack = r1low;
+
+ sr1high -= ALPHAM_80;
+ double sr1high_stack = sr1high;
+
+ r2low -= r2high;
+ double r2low_stack = r2low;
+
+ sr2high -= ALPHAM_48;
+ double sr2high_stack = sr2high;
+
+ double r3high = r3low + ALPHA_112;
+ double r0low_stack = r0low;
+
+ sr1low -= sr1high;
+ double sr1low_stack = sr1low;
+
+ double sr3high = sr3low + ALPHAM_16;
+ double r2high_stack = r2high;
+
+ sr2low -= sr2high;
+ double sr2low_stack = sr2low;
+
+ r3high -= ALPHA_112;
+ double r3high_stack = r3high;
+
+ sr3high -= ALPHAM_16;
+ double sr3high_stack = sr3high;
+
+ r3low -= r3high;
+ double r3low_stack = r3low;
+
+ sr3low -= sr3high;
+ double sr3low_stack = sr3low;
+
+
+ if (!(l < 16)) {
+ m00 = 0xFF & m[(int) p];
+ m0 = 2151;
+
+ m0 <<= 51;
+ m1 = 2215;
+ m01 = 0xFF & m[(int) p + 1];
+
+ m1 <<= 51;
+ m2 = 2279;
+ m02 = 0xFF & m[(int) p + 2];
+
+ m2 <<= 51;
+ m3 = 2343;
+ m03 = 0xFF & (m[(int) p + 3]);
+
+ m10 = 0xFF & (m[(int) p + 4]);
+ m01 <<= 8;
+ m0 += m00;
+
+ m11 = 0xFF & (m[(int) p + 5]);
+ m02 <<= 16;
+ m0 += m01;
+
+ m12 = 0xFF & (m[(int) p + 6]);
+ m03 <<= 24;
+ m0 += m02;
+
+ m13 = 0xFF & (m[(int) p + 7]);
+ m3 <<= 51;
+ m0 += m03;
+
+ m20 = 0xFF & (m[(int) p + 8]);
+ m11 <<= 8;
+ m1 += m10;
+
+ m21 = 0xFF & (m[(int) p + 9]);
+ m12 <<= 16;
+ m1 += m11;
+
+ m22 = 0xFF & (m[(int) p + 10]);
+ m13 <<= 24;
+ m1 += m12;
+
+ m23 = 0xFF & (m[(int) p + 11]);
+ m1 += m13;
+
+ m30 = 0xFF & (m[(int) p + 12]);
+ m21 <<= 8;
+ m2 += m20;
+
+ m31 = 0xFF & (m[(int) p + 13]);
+ m22 <<= 16;
+ m2 += m21;
+
+ m32 = 0xFF & (m[(int) p + 14]);
+ m23 <<= 24;
+ m2 += m22;
+
+ m33 = 0xFF & (m[(int) p + 15]);
+ m2 += m23;
+
+ d0 = m0;
+ m31 <<= 8;
+ m3 += m30;
+
+ d1 = m1;
+ m32 <<= 16;
+ m3 += m31;
+
+ d2 = m2;
+ m33 += 256;
+
+ m33 <<= 24;
+ m3 += m32;
+
+ m3 += m33;
+ d3 = m3;
+
+ p += 16;
+ l -= 16;
+
+ z0 = Double.longBitsToDouble(d0);
+
+ z1 = Double.longBitsToDouble(d1);
+
+ z2 = Double.longBitsToDouble(d2);
+
+ z3 = Double.longBitsToDouble(d3);
+
+ z0 -= ALPHA_0;
+
+ z1 -= ALPHA_32;
+
+ z2 -= ALPHA_64;
+
+ z3 -= ALPHA_96;
+
+ h0 += z0;
+
+ h1 += z1;
+
+ h3 += z2;
+
+ h5 += z3;
+
+ while (l >= 16) {
+ //multiplyaddatleast16bytes:
+
+ m2 = 2279;
+ m20 = 0xFF & (m[(int) p + 8]);
+ y7 = h7 + ALPHA_130;
+
+ m2 <<= 51;
+ m3 = 2343;
+ m21 = 0xFF & (m[(int) p + 9]);
+ y6 = h6 + ALPHA_130;
+
+ m3 <<= 51;
+ m0 = 2151;
+ m22 = 0xFF & (m[(int) p + 10]);
+ y1 = h1 + ALPHA_32;
+
+ m0 <<= 51;
+ m1 = 2215;
+ m23 = 0xFF & (m[(int) p + 11]);
+ y0 = h0 + ALPHA_32;
+
+ m1 <<= 51;
+ m30 = 0xFF & (m[(int) p + 12]);
+ y7 -= ALPHA_130;
+
+ m21 <<= 8;
+ m2 += m20;
+ m31 = 0xFF & (m[(int) p + 13]);
+ y6 -= ALPHA_130;
+
+ m22 <<= 16;
+ m2 += m21;
+ m32 = 0xFF & (m[(int) p + 14]);
+ y1 -= ALPHA_32;
+
+ m23 <<= 24;
+ m2 += m22;
+ m33 = 0xFF & (m[(int) p + 15]);
+ y0 -= ALPHA_32;
+
+ m2 += m23;
+ m00 = 0xFF & (m[(int) p]);
+ y5 = h5 + ALPHA_96;
+
+ m31 <<= 8;
+ m3 += m30;
+ m01 = 0xFF & (m[(int) p + 1]);
+ y4 = h4 + ALPHA_96;
+
+ m32 <<= 16;
+ m02 = 0xFF & (m[(int) p + 2]);
+ x7 = h7 - y7;
+ y7 *= SCALE;
+
+ m33 += 256;
+ m03 = 0xFF & (m[(int) p + 3]);
+ x6 = h6 - y6;
+ y6 *= SCALE;
+
+ m33 <<= 24;
+ m3 += m31;
+ m10 = 0xFF & (m[(int) p + 4]);
+ x1 = h1 - y1;
+
+ m01 <<= 8;
+ m3 += m32;
+ m11 = 0xFF & (m[(int) p + 5]);
+ x0 = h0 - y0;
+
+ m3 += m33;
+ m0 += m00;
+ m12 = 0xFF & (m[(int) p + 6]);
+ y5 -= ALPHA_96;
+
+ m02 <<= 16;
+ m0 += m01;
+ m13 = 0xFF & (m[(int) p + 7]);
+ y4 -= ALPHA_96;
+
+ m03 <<= 24;
+ m0 += m02;
+ d2 = m2;
+ x1 += y7;
+
+ m0 += m03;
+ d3 = m3;
+ x0 += y6;
+
+ m11 <<= 8;
+ m1 += m10;
+ d0 = m0;
+ x7 += y5;
+
+ m12 <<= 16;
+ m1 += m11;
+ x6 += y4;
+
+ m13 <<= 24;
+ m1 += m12;
+ y3 = h3 + ALPHA_64;
+
+ m1 += m13;
+ d1 = m1;
+ y2 = h2 + ALPHA_64;
+
+ x0 += x1;
+
+ x6 += x7;
+
+ y3 -= ALPHA_64;
+ r3low = r3low_stack;
+
+ y2 -= ALPHA_64;
+ r0low = r0low_stack;
+
+ x5 = h5 - y5;
+ r3lowx0 = r3low * x0;
+ r3high = r3high_stack;
+
+ x4 = h4 - y4;
+ r0lowx6 = r0low * x6;
+ r0high = r0high_stack;
+
+ x3 = h3 - y3;
+ r3highx0 = r3high * x0;
+ sr1low = sr1low_stack;
+
+ x2 = h2 - y2;
+ r0highx6 = r0high * x6;
+ sr1high = sr1high_stack;
+
+ x5 += y3;
+ r0lowx0 = r0low * x0;
+ r1low = r1low_stack;
+
+ h6 = r3lowx0 + r0lowx6;
+ sr1lowx6 = sr1low * x6;
+ r1high = r1high_stack;
+
+ x4 += y2;
+ r0highx0 = r0high * x0;
+ sr2low = sr2low_stack;
+
+ h7 = r3highx0 + r0highx6;
+ sr1highx6 = sr1high * x6;
+ sr2high = sr2high_stack;
+
+ x3 += y1;
+ r1lowx0 = r1low * x0;
+ r2low = r2low_stack;
+
+ h0 = r0lowx0 + sr1lowx6;
+ sr2lowx6 = sr2low * x6;
+ r2high = r2high_stack;
+
+ x2 += y0;
+ r1highx0 = r1high * x0;
+ sr3low = sr3low_stack;
+
+ h1 = r0highx0 + sr1highx6;
+ sr2highx6 = sr2high * x6;
+ sr3high = sr3high_stack;
+
+ x4 += x5;
+ r2lowx0 = r2low * x0;
+ z2 = Double.longBitsToDouble(d2);
+
+ h2 = r1lowx0 + sr2lowx6;
+ sr3lowx6 = sr3low * x6;
+
+ x2 += x3;
+ r2highx0 = r2high * x0;
+ z3 = Double.longBitsToDouble(d3);
+
+ h3 = r1highx0 + sr2highx6;
+ sr3highx6 = sr3high * x6;
+
+ r1highx4 = r1high * x4;
+ z2 -= ALPHA_64;
+
+ h4 = r2lowx0 + sr3lowx6;
+ r1lowx4 = r1low * x4;
+
+ r0highx4 = r0high * x4;
+ z3 -= ALPHA_96;
+
+ h5 = r2highx0 + sr3highx6;
+ r0lowx4 = r0low * x4;
+
+ h7 += r1highx4;
+ sr3highx4 = sr3high * x4;
+
+ h6 += r1lowx4;
+ sr3lowx4 = sr3low * x4;
+
+ h5 += r0highx4;
+ sr2highx4 = sr2high * x4;
+
+ h4 += r0lowx4;
+ sr2lowx4 = sr2low * x4;
+
+ h3 += sr3highx4;
+ r0lowx2 = r0low * x2;
+
+ h2 += sr3lowx4;
+ r0highx2 = r0high * x2;
+
+ h1 += sr2highx4;
+ r1lowx2 = r1low * x2;
+
+ h0 += sr2lowx4;
+ r1highx2 = r1high * x2;
+
+ h2 += r0lowx2;
+ r2lowx2 = r2low * x2;
+
+ h3 += r0highx2;
+ r2highx2 = r2high * x2;
+
+ h4 += r1lowx2;
+ sr3lowx2 = sr3low * x2;
+
+ h5 += r1highx2;
+ sr3highx2 = sr3high * x2;
+
+ p += 16;
+ l -= 16;
+ h6 += r2lowx2;
+
+ h7 += r2highx2;
+
+ z1 = Double.longBitsToDouble(d1);
+ h0 += sr3lowx2;
+
+ z0 = Double.longBitsToDouble(d0);
+ h1 += sr3highx2;
+
+ z1 -= ALPHA_32;
+
+ z0 -= ALPHA_0;
+
+ h5 += z3;
+
+ h3 += z2;
+
+ h1 += z1;
+
+ h0 += z0;
+
+ }
+
+ // multiplyaddatmost15bytes:
+ y7 = h7 + ALPHA_130;
+
+ y6 = h6 + ALPHA_130;
+
+ y1 = h1 + ALPHA_32;
+
+ y0 = h0 + ALPHA_32;
+
+ y7 -= ALPHA_130;
+
+ y6 -= ALPHA_130;
+
+ y1 -= ALPHA_32;
+
+ y0 -= ALPHA_32;
+
+ y5 = h5 + ALPHA_96;
+
+ y4 = h4 + ALPHA_96;
+
+ x7 = h7 - y7;
+ y7 *= SCALE;
+
+ x6 = h6 - y6;
+ y6 *= SCALE;
+
+ x1 = h1 - y1;
+
+ x0 = h0 - y0;
+
+ y5 -= ALPHA_96;
+
+ y4 -= ALPHA_96;
+
+ x1 += y7;
+
+ x0 += y6;
+
+ x7 += y5;
+
+ x6 += y4;
+
+ y3 = h3 + ALPHA_64;
+
+ y2 = h2 + ALPHA_64;
+
+ x0 += x1;
+
+ x6 += x7;
+
+ y3 -= ALPHA_64;
+ r3low = r3low_stack;
+
+ y2 -= ALPHA_64;
+ r0low = r0low_stack;
+
+ x5 = h5 - y5;
+ r3lowx0 = r3low * x0;
+ r3high = r3high_stack;
+
+ x4 = h4 - y4;
+ r0lowx6 = r0low * x6;
+ r0high = r0high_stack;
+
+ x3 = h3 - y3;
+ r3highx0 = r3high * x0;
+ sr1low = sr1low_stack;
+
+ x2 = h2 - y2;
+ r0highx6 = r0high * x6;
+ sr1high = sr1high_stack;
+
+ x5 += y3;
+ r0lowx0 = r0low * x0;
+ r1low = r1low_stack;
+
+ h6 = r3lowx0 + r0lowx6;
+ sr1lowx6 = sr1low * x6;
+ r1high = r1high_stack;
+
+ x4 += y2;
+ r0highx0 = r0high * x0;
+ sr2low = sr2low_stack;
+
+ h7 = r3highx0 + r0highx6;
+ sr1highx6 = sr1high * x6;
+ sr2high = sr2high_stack;
+
+ x3 += y1;
+ r1lowx0 = r1low * x0;
+ r2low = r2low_stack;
+
+ h0 = r0lowx0 + sr1lowx6;
+ sr2lowx6 = sr2low * x6;
+ r2high = r2high_stack;
+
+ x2 += y0;
+ r1highx0 = r1high * x0;
+ sr3low = sr3low_stack;
+
+ h1 = r0highx0 + sr1highx6;
+ sr2highx6 = sr2high * x6;
+ sr3high = sr3high_stack;
+
+ x4 += x5;
+ r2lowx0 = r2low * x0;
+
+ h2 = r1lowx0 + sr2lowx6;
+ sr3lowx6 = sr3low * x6;
+
+ x2 += x3;
+ r2highx0 = r2high * x0;
+
+ h3 = r1highx0 + sr2highx6;
+ sr3highx6 = sr3high * x6;
+
+ r1highx4 = r1high * x4;
+
+ h4 = r2lowx0 + sr3lowx6;
+ r1lowx4 = r1low * x4;
+
+ r0highx4 = r0high * x4;
+
+ h5 = r2highx0 + sr3highx6;
+ r0lowx4 = r0low * x4;
+
+ h7 += r1highx4;
+ sr3highx4 = sr3high * x4;
+
+ h6 += r1lowx4;
+ sr3lowx4 = sr3low * x4;
+
+ h5 += r0highx4;
+ sr2highx4 = sr2high * x4;
+
+ h4 += r0lowx4;
+ sr2lowx4 = sr2low * x4;
+
+ h3 += sr3highx4;
+ r0lowx2 = r0low * x2;
+
+ h2 += sr3lowx4;
+ r0highx2 = r0high * x2;
+
+ h1 += sr2highx4;
+ r1lowx2 = r1low * x2;
+
+ h0 += sr2lowx4;
+ r1highx2 = r1high * x2;
+
+ h2 += r0lowx2;
+ r2lowx2 = r2low * x2;
+
+ h3 += r0highx2;
+ r2highx2 = r2high * x2;
+
+ h4 += r1lowx2;
+ sr3lowx2 = sr3low * x2;
+
+ h5 += r1highx2;
+ sr3highx2 = sr3high * x2;
+
+ h6 += r2lowx2;
+
+ h7 += r2highx2;
+
+ h0 += sr3lowx2;
+
+ h1 += sr3highx2;
+ }
+
+ // addatmost15bytes:
+
+ if (l > 0) {
+ lbelow2 = l - 2;
+
+ lbelow3 = l - 3;
+
+ lbelow2 >>= 31;
+ lbelow4 = l - 4;
+
+ m00 = 0xFF & (m[(int) p]);
+ lbelow3 >>= 31;
+ p += lbelow2;
+
+ m01 = 0xFF & (m[(int) p + 1]);
+ lbelow4 >>= 31;
+ p += lbelow3;
+
+ m02 = 0xFF & (m[(int) p + 2]);
+ p += lbelow4;
+ m0 = 2151;
+
+ m03 = 0xFF & (m[(int) p + 3]);
+ m0 <<= 51;
+ m1 = 2215;
+
+ m0 += m00;
+ m01 &= ~uint32(lbelow2);
+
+ m02 &= ~uint32(lbelow3);
+ m01 -= uint32(lbelow2);
+
+ m01 <<= 8;
+ m03 &= ~uint32(lbelow4);
+
+ m0 += m01;
+ lbelow2 -= lbelow3;
+
+ m02 += uint32(lbelow2);
+ lbelow3 -= lbelow4;
+
+ m02 <<= 16;
+ m03 += uint32(lbelow3);
+
+ m03 <<= 24;
+ m0 += m02;
+
+ m0 += m03;
+ lbelow5 = l - 5;
+
+ lbelow6 = l - 6;
+ lbelow7 = l - 7;
+
+ lbelow5 >>= 31;
+ lbelow8 = l - 8;
+
+ lbelow6 >>= 31;
+ p += lbelow5;
+
+ m10 = 0xFF & (m[(int) p + 4]);
+ lbelow7 >>= 31;
+ p += lbelow6;
+
+ m11 = 0xFF & (m[(int) p + 5]);
+ lbelow8 >>= 31;
+ p += lbelow7;
+
+ m12 = 0xFF & (m[(int) p + 6]);
+ m1 <<= 51;
+ p += lbelow8;
+
+ m13 = 0xFF & (m[(int) p + 7]);
+ m10 &= ~uint32(lbelow5);
+ lbelow4 -= lbelow5;
+
+ m10 += uint32(lbelow4);
+ lbelow5 -= lbelow6;
+
+ m11 &= ~uint32(lbelow6);
+ m11 += uint32(lbelow5);
+
+ m11 <<= 8;
+ m1 += m10;
+
+ m1 += m11;
+ m12 &= ~uint32(lbelow7);
+
+ lbelow6 -= lbelow7;
+ m13 &= ~uint32(lbelow8);
+
+ m12 += uint32(lbelow6);
+ lbelow7 -= lbelow8;
+
+ m12 <<= 16;
+ m13 += uint32(lbelow7);
+
+ m13 <<= 24;
+ m1 += m12;
+
+ m1 += m13;
+ m2 = 2279;
+
+ lbelow9 = l - 9;
+ m3 = 2343;
+
+ lbelow10 = l - 10;
+ lbelow11 = l - 11;
+
+ lbelow9 >>= 31;
+ lbelow12 = l - 12;
+
+ lbelow10 >>= 31;
+ p += lbelow9;
+
+ m20 = 0xFF & (m[(int) p + 8]);
+ lbelow11 >>= 31;
+ p += lbelow10;
+
+ m21 = 0xFF & (m[(int) p + 9]);
+ lbelow12 >>= 31;
+ p += lbelow11;
+
+ m22 = 0xFF & (m[(int) p + 10]);
+ m2 <<= 51;
+ p += lbelow12;
+
+ m23 = 0xFF & (m[(int) p + 11]);
+ m20 &= ~uint32(lbelow9);
+ lbelow8 -= lbelow9;
+
+ m20 += uint32(lbelow8);
+ lbelow9 -= lbelow10;
+
+ m21 &= ~uint32(lbelow10);
+ m21 += uint32(lbelow9);
+
+ m21 <<= 8;
+ m2 += m20;
+
+ m2 += m21;
+ m22 &= ~uint32(lbelow11);
+
+ lbelow10 -= lbelow11;
+ m23 &= ~uint32(lbelow12);
+
+ m22 += uint32(lbelow10);
+ lbelow11 -= lbelow12;
+
+ m22 <<= 16;
+ m23 += uint32(lbelow11);
+
+ m23 <<= 24;
+ m2 += m22;
+
+ m3 <<= 51;
+ lbelow13 = l - 13;
+
+ lbelow13 >>= 31;
+ lbelow14 = l - 14;
+
+ lbelow14 >>= 31;
+ p += lbelow13;
+ lbelow15 = l - 15;
+
+ m30 = uint32(m[(int) p + 12]);
+ lbelow15 >>= 31;
+ p += lbelow14;
+
+ m31 = 0xFF & (m[(int) p + 13]);
+ p += lbelow15;
+ m2 += m23;
+
+ m32 = 0xFF & (m[(int) p + 14]);
+ m30 &= ~uint32(lbelow13);
+ lbelow12 -= lbelow13;
+
+ m30 += uint32(lbelow12);
+ lbelow13 -= lbelow14;
+
+ m3 += m30;
+ m31 &= ~uint32(lbelow14);
+
+ m31 += uint32(lbelow13);
+ m32 &= ~uint32(lbelow15);
+
+ m31 <<= 8;
+ lbelow14 -= lbelow15;
+
+ m3 += m31;
+ m32 += uint32(lbelow14);
+ d0 = m0;
+
+ m32 <<= 16;
+ m33 = lbelow15 + 1;
+ d1 = m1;
+
+ m33 <<= 24;
+ m3 += m32;
+ d2 = m2;
+
+ m3 += m33;
+ d3 = m3;
+
+ z3 = Double.longBitsToDouble(d3);
+
+ z2 = Double.longBitsToDouble(d2);
+
+ z1 = Double.longBitsToDouble(d1);
+
+ z0 = Double.longBitsToDouble(d0);
+
+ z3 -= ALPHA_96;
+
+ z2 -= ALPHA_64;
+
+ z1 -= ALPHA_32;
+
+ z0 -= ALPHA_0;
+
+ h5 += z3;
+
+ h3 += z2;
+
+ h1 += z1;
+
+ h0 += z0;
+
+ y7 = h7 + ALPHA_130;
+
+ y6 = h6 + ALPHA_130;
+
+ y1 = h1 + ALPHA_32;
+
+ y0 = h0 + ALPHA_32;
+
+ y7 -= ALPHA_130;
+
+ y6 -= ALPHA_130;
+
+ y1 -= ALPHA_32;
+
+ y0 -= ALPHA_32;
+
+ y5 = h5 + ALPHA_96;
+
+ y4 = h4 + ALPHA_96;
+
+ x7 = h7 - y7;
+ y7 *= SCALE;
+
+ x6 = h6 - y6;
+ y6 *= SCALE;
+
+ x1 = h1 - y1;
+
+ x0 = h0 - y0;
+
+ y5 -= ALPHA_96;
+
+ y4 -= ALPHA_96;
+
+ x1 += y7;
+
+ x0 += y6;
+
+ x7 += y5;
+
+ x6 += y4;
+
+ y3 = h3 + ALPHA_64;
+
+ y2 = h2 + ALPHA_64;
+
+ x0 += x1;
+
+ x6 += x7;
+
+ y3 -= ALPHA_64;
+ r3low = r3low_stack;
+
+ y2 -= ALPHA_64;
+ r0low = r0low_stack;
+
+ x5 = h5 - y5;
+ r3lowx0 = r3low * x0;
+ r3high = r3high_stack;
+
+ x4 = h4 - y4;
+ r0lowx6 = r0low * x6;
+ r0high = r0high_stack;
+
+ x3 = h3 - y3;
+ r3highx0 = r3high * x0;
+ sr1low = sr1low_stack;
+
+ x2 = h2 - y2;
+ r0highx6 = r0high * x6;
+ sr1high = sr1high_stack;
+
+ x5 += y3;
+ r0lowx0 = r0low * x0;
+ r1low = r1low_stack;
+
+ h6 = r3lowx0 + r0lowx6;
+ sr1lowx6 = sr1low * x6;
+ r1high = r1high_stack;
+
+ x4 += y2;
+ r0highx0 = r0high * x0;
+ sr2low = sr2low_stack;
+
+ h7 = r3highx0 + r0highx6;
+ sr1highx6 = sr1high * x6;
+ sr2high = sr2high_stack;
+
+ x3 += y1;
+ r1lowx0 = r1low * x0;
+ r2low = r2low_stack;
+
+ h0 = r0lowx0 + sr1lowx6;
+ sr2lowx6 = sr2low * x6;
+ r2high = r2high_stack;
+
+ x2 += y0;
+ r1highx0 = r1high * x0;
+ sr3low = sr3low_stack;
+
+ h1 = r0highx0 + sr1highx6;
+ sr2highx6 = sr2high * x6;
+ sr3high = sr3high_stack;
+
+ x4 += x5;
+ r2lowx0 = r2low * x0;
+
+ h2 = r1lowx0 + sr2lowx6;
+ sr3lowx6 = sr3low * x6;
+
+ x2 += x3;
+ r2highx0 = r2high * x0;
+
+ h3 = r1highx0 + sr2highx6;
+ sr3highx6 = sr3high * x6;
+
+ r1highx4 = r1high * x4;
+
+ h4 = r2lowx0 + sr3lowx6;
+ r1lowx4 = r1low * x4;
+
+ r0highx4 = r0high * x4;
+
+ h5 = r2highx0 + sr3highx6;
+ r0lowx4 = r0low * x4;
+
+ h7 += r1highx4;
+ sr3highx4 = sr3high * x4;
+
+ h6 += r1lowx4;
+ sr3lowx4 = sr3low * x4;
+
+ h5 += r0highx4;
+ sr2highx4 = sr2high * x4;
+
+ h4 += r0lowx4;
+ sr2lowx4 = sr2low * x4;
+
+ h3 += sr3highx4;
+ r0lowx2 = r0low * x2;
+
+ h2 += sr3lowx4;
+ r0highx2 = r0high * x2;
+
+ h1 += sr2highx4;
+ r1lowx2 = r1low * x2;
+
+ h0 += sr2lowx4;
+ r1highx2 = r1high * x2;
+
+ h2 += r0lowx2;
+ r2lowx2 = r2low * x2;
+
+ h3 += r0highx2;
+ r2highx2 = r2high * x2;
+
+ h4 += r1lowx2;
+ sr3lowx2 = sr3low * x2;
+
+ h5 += r1highx2;
+ sr3highx2 = sr3high * x2;
+
+ h6 += r2lowx2;
+
+ h7 += r2highx2;
+
+ h0 += sr3lowx2;
+
+ h1 += sr3highx2;
+ }
+
+ //nomorebytes:
+
+ y7 = h7 + ALPHA_130;
+
+ y0 = h0 + ALPHA_32;
+
+ y1 = h1 + ALPHA_32;
+
+ y2 = h2 + ALPHA_64;
+
+ y7 -= ALPHA_130;
+
+ y3 = h3 + ALPHA_64;
+
+ y4 = h4 + ALPHA_96;
+
+ y5 = h5 + ALPHA_96;
+
+ x7 = h7 - y7;
+ y7 *= SCALE;
+
+ y0 -= ALPHA_32;
+
+ y1 -= ALPHA_32;
+
+ y2 -= ALPHA_64;
+
+ h6 += x7;
+
+ y3 -= ALPHA_64;
+
+ y4 -= ALPHA_96;
+
+ y5 -= ALPHA_96;
+
+ y6 = h6 + ALPHA_130;
+
+ x0 = h0 - y0;
+
+ x1 = h1 - y1;
+
+ x2 = h2 - y2;
+
+ y6 -= ALPHA_130;
+
+ x0 += y7;
+
+ x3 = h3 - y3;
+
+ x4 = h4 - y4;
+
+ x5 = h5 - y5;
+
+ x6 = h6 - y6;
+
+ y6 *= SCALE;
+
+ x2 += y0;
+
+ x3 += y1;
+
+ x4 += y2;
+
+ x0 += y6;
+
+ x5 += y3;
+
+ x6 += y4;
+
+ x2 += x3;
+
+ x0 += x1;
+
+ x4 += x5;
+
+ x6 += y5;
+
+ x2 += OFFSET_1;
+ d1 = Double.doubleToLongBits(x2);
+
+ x0 += OFFSET_0;
+ d0 = Double.doubleToLongBits(x0);
+
+ x4 += OFFSET_2;
+ d2 = Double.doubleToLongBits(x4);
+
+ x6 += OFFSET_3;
+ d3 = Double.doubleToLongBits(x6);
+
+ f0 = d0;
+
+ f1 = d1;
+ bits32 = 0xFFFFFFFFFFFFFFFFL;
+
+ f2 = d2;
+ bits32 >>>= 32;
+
+ f3 = d3;
+ f = f0 >> 32;
+
+ f0 &= bits32;
+ f &= 255;
+
+ f1 += f;
+ g0 = f0 + 5;
+
+ g = g0 >> 32;
+ g0 &= bits32;
+
+ f = f1 >> 32;
+ f1 &= bits32;
+
+ f &= 255;
+ g1 = f1 + g;
+
+ g = g1 >> 32;
+ f2 += f;
+
+ f = f2 >> 32;
+ g1 &= bits32;
+
+ f2 &= bits32;
+ f &= 255;
+
+ f3 += f;
+ g2 = f2 + g;
+
+ g = g2 >> 32;
+ g2 &= bits32;
+
+ f4 = f3 >> 32;
+ f3 &= bits32;
+
+ f4 &= 255;
+ g3 = f3 + g;
+
+ g = g3 >> 32;
+ g3 &= bits32;
+
+ g4 = f4 + g;
+
+ g4 = g4 - 4;
+ s00 = 0xFF & (s[0]);
+
+ f = g4 >> 63;
+ s01 = 0xFF & (s[1]);
+
+ f0 &= f;
+ g0 &= ~f;
+ s02 = 0xFF & (s[2]);
+
+ f1 &= f;
+ f0 |= g0;
+ s03 = 0xFF & (s[3]);
+
+ g1 &= ~f;
+ f2 &= f;
+ s10 = 0xFF & (s[4]);
+
+ f3 &= f;
+ g2 &= ~f;
+ s11 = 0xFF & (s[5]);
+
+ g3 &= ~f;
+ f1 |= g1;
+ s12 = 0xFF & (s[6]);
+
+ f2 |= g2;
+ f3 |= g3;
+ s13 = 0xFF & (s[7]);
+
+ s01 <<= 8;
+ f0 += s00;
+ s20 = 0xFF & (s[8]);
+
+ s02 <<= 16;
+ f0 += s01;
+ s21 = 0xFF & (s[9]);
+
+ s03 <<= 24;
+ f0 += s02;
+ s22 = 0xFF & (s[10]);
+
+ s11 <<= 8;
+ f1 += s10;
+ s23 = 0xFF & (s[11]);
+
+ s12 <<= 16;
+ f1 += s11;
+ s30 = 0xFF & (s[12]);
+
+ s13 <<= 24;
+ f1 += (s12);
+ s31 = 0xFF & s[13];
+
+ f0 += (s03);
+ f1 += (s13);
+ s32 = 0xFF & (s[14]);
+
+ s21 <<= 8;
+ f2 += (s20);
+ s33 = 0xFF & (s[15]);
+
+ s22 <<= 16;
+ f2 += (s21);
+
+ s23 <<= 24;
+ f2 += (s22);
+
+ s31 <<= 8;
+ f3 += (s30);
+
+ s32 <<= 16;
+ f3 += (s31);
+
+ s33 <<= 24;
+ f3 += (s32);
+
+ f2 += (s23);
+ f3 += s33;
+
+ byte[] out = new byte[16];
+ out[0] = (byte) (f0);
+ f0 >>= 8;
+ out[1] = (byte) (f0);
+ f0 >>= 8;
+ out[2] = (byte) (f0);
+ f0 >>= 8;
+ out[3] = (byte) (f0);
+ f0 >>= 8;
+ f1 += f0;
+
+ out[4] = (byte) (f1);
+ f1 >>= 8;
+ out[5] = (byte) (f1);
+ f1 >>= 8;
+ out[6] = (byte) (f1);
+ f1 >>= 8;
+ out[7] = (byte) (f1);
+ f1 >>= 8;
+ f2 += f1;
+
+ out[8] = (byte) (f2);
+ f2 >>= 8;
+ out[9] = (byte) (f2);
+ f2 >>= 8;
+ out[10] = (byte) (f2);
+ f2 >>= 8;
+ out[11] = (byte) (f2);
+ f2 >>= 8;
+ f3 += f2;
+
+ out[12] = (byte) f3;
+ f3 >>= 8;
+ out[13] = (byte) f3;
+ f3 >>= 8;
+ out[14] = (byte) f3;
+ f3 >>= 8;
+ out[15] = (byte) f3;
+ return out;
+ }
+}
diff --git a/src/main/java/com/pusher/client/crypto/nacl/Salsa.java b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java
new file mode 100644
index 00000000..873ebd83
--- /dev/null
+++ b/src/main/java/com/pusher/client/crypto/nacl/Salsa.java
@@ -0,0 +1,431 @@
+/*
+Copyright 2015 Eve Freeman
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+*/
+
+package com.pusher.client.crypto.nacl;
+
+public class Salsa {
+ public static final byte[] SIGMA = {'e', 'x', 'p', 'a', 'n', 'd', ' ', '3', '2', '-', 'b', 'y', 't', 'e', ' ', 'k'};
+
+ private static final int ROUNDS = 20;
+
+ private static long mask(byte x) {
+ return 0xFFL & x;
+ }
+
+ // core applies the Salsa20 core function to 16-byte input in, 32-byte key k,
+ // and 16-byte constant c, and puts the result into 64-byte array out.
+ public static byte[] core(byte[] in, byte[] k, byte[] c) {
+ byte[] out = new byte[64];
+ long mask = 0xFFFFFFFFL;
+
+ long j0 = mask & (mask(c[0]) | mask(c[1]) << 8 | mask(c[2]) << 16 | mask(c[3]) << 24);
+ long j1 = mask & (mask(k[0]) | mask(k[1]) << 8 | mask(k[2]) << 16 | mask(k[3]) << 24);
+ long j2 = mask & (mask(k[4]) | mask(k[5]) << 8 | mask(k[6]) << 16 | mask(k[7]) << 24);
+ long j3 = mask & (mask(k[8]) | mask(k[9]) << 8 | mask(k[10]) << 16 | mask(k[11]) << 24);
+ long j4 = mask & (mask(k[12]) | mask(k[13]) << 8 | mask(k[14]) << 16 | mask(k[15]) << 24);
+ long j5 = mask & (mask(c[4]) | mask(c[5]) << 8 | mask(c[6]) << 16 | mask(c[7]) << 24);
+ long j6 = mask & (mask(in[0]) | mask(in[1]) << 8 | mask(in[2]) << 16 | mask(in[3]) << 24);
+ long j7 = mask & (mask(in[4]) | mask(in[5]) << 8 | mask(in[6]) << 16 | mask(in[7]) << 24);
+ long j8 = mask & (mask(in[8]) | mask(in[9]) << 8 | mask(in[10]) << 16 | mask(in[11]) << 24);
+ long j9 = mask & (mask(in[12]) | mask(in[13]) << 8 | mask(in[14]) << 16 | mask(in[15]) << 24);
+ long j10 = mask & (mask(c[8]) | mask(c[9]) << 8 | mask(c[10]) << 16 | mask(c[11]) << 24);
+ long j11 = mask & (mask(k[16]) | mask(k[17]) << 8 | mask(k[18]) << 16 | mask(k[19]) << 24);
+ long j12 = mask & (mask(k[20]) | mask(k[21]) << 8 | mask(k[22]) << 16 | mask(k[23]) << 24);
+ long j13 = mask & (mask(k[24]) | mask(k[25]) << 8 | mask(k[26]) << 16 | mask(k[27]) << 24);
+ long j14 = mask & (mask(k[28]) | mask(k[29]) << 8 | mask(k[30]) << 16 | mask(k[31]) << 24);
+ long j15 = mask & (mask(c[12]) | mask(c[13]) << 8 | mask(c[14]) << 16 | mask(c[15]) << 24);
+
+ long x0 = j0, x1 = j1, x2 = j2, x3 = j3, x4 = j4;
+ long x5 = j5, x6 = j6, x7 = j7, x8 = j8;
+ long x9 = j9, x10 = j10, x11 = j11, x12 = j12;
+ long x13 = j13, x14 = j14, x15 = j15;
+
+ for (int i = 0; i < ROUNDS; i += 2) {
+ long u = mask & (x0 + x12);
+ x4 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x4 + x0);
+ x8 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x8 + x4);
+ x12 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x12 + x8);
+ x0 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x5 + x1);
+ x9 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x9 + x5);
+ x13 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x13 + x9);
+ x1 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x1 + x13);
+ x5 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x10 + x6);
+ x14 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x14 + x10);
+ x2 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x2 + x14);
+ x6 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x6 + x2);
+ x10 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x15 + x11);
+ x3 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x3 + x15);
+ x7 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x7 + x3);
+ x11 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x11 + x7);
+ x15 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x0 + x3);
+ x1 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x1 + x0);
+ x2 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x2 + x1);
+ x3 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x3 + x2);
+ x0 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x5 + x4);
+ x6 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x6 + x5);
+ x7 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x7 + x6);
+ x4 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x4 + x7);
+ x5 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x10 + x9);
+ x11 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x11 + x10);
+ x8 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x8 + x11);
+ x9 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x9 + x8);
+ x10 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x15 + x14);
+ x12 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x12 + x15);
+ x13 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x13 + x12);
+ x14 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x14 + x13);
+ x15 ^= mask & (u << 18 | u >>> (32 - 18));
+ }
+
+ x0 += j0;
+ x1 += j1;
+ x2 += j2;
+ x3 += j3;
+ x4 += j4;
+ x5 += j5;
+ x6 += j6;
+ x7 += j7;
+ x8 += j8;
+ x9 += j9;
+ x10 += j10;
+ x11 += j11;
+ x12 += j12;
+ x13 += j13;
+ x14 += j14;
+ x15 += j15;
+
+ x0 &= mask;
+ x1 &= mask;
+ x2 &= mask;
+ x3 &= mask;
+ x4 &= mask;
+ x5 &= mask;
+ x6 &= mask;
+ x7 &= mask;
+ x8 &= mask;
+ x9 &= mask;
+ x10 &= mask;
+ x11 &= mask;
+ x12 &= mask;
+ x13 &= mask;
+ x14 &= mask;
+ x15 &= mask;
+
+ out[0] = (byte) (x0);
+ out[1] = (byte) (x0 >> 8);
+ out[2] = (byte) (x0 >> 16);
+ out[3] = (byte) (x0 >> 24);
+
+ out[4] = (byte) (x1);
+ out[5] = (byte) (x1 >> 8);
+ out[6] = (byte) (x1 >> 16);
+ out[7] = (byte) (x1 >> 24);
+
+ out[8] = (byte) (x2);
+ out[9] = (byte) (x2 >> 8);
+ out[10] = (byte) (x2 >> 16);
+ out[11] = (byte) (x2 >> 24);
+
+ out[12] = (byte) (x3);
+ out[13] = (byte) (x3 >> 8);
+ out[14] = (byte) (x3 >> 16);
+ out[15] = (byte) (x3 >> 24);
+
+ out[16] = (byte) (x4);
+ out[17] = (byte) (x4 >> 8);
+ out[18] = (byte) (x4 >> 16);
+ out[19] = (byte) (x4 >> 24);
+
+ out[20] = (byte) (x5);
+ out[21] = (byte) (x5 >> 8);
+ out[22] = (byte) (x5 >> 16);
+ out[23] = (byte) (x5 >> 24);
+
+ out[24] = (byte) (x6);
+ out[25] = (byte) (x6 >> 8);
+ out[26] = (byte) (x6 >> 16);
+ out[27] = (byte) (x6 >> 24);
+
+ out[28] = (byte) (x7);
+ out[29] = (byte) (x7 >> 8);
+ out[30] = (byte) (x7 >> 16);
+ out[31] = (byte) (x7 >> 24);
+
+ out[32] = (byte) (x8);
+ out[33] = (byte) (x8 >> 8);
+ out[34] = (byte) (x8 >> 16);
+ out[35] = (byte) (x8 >> 24);
+
+ out[36] = (byte) (x9);
+ out[37] = (byte) (x9 >> 8);
+ out[38] = (byte) (x9 >> 16);
+ out[39] = (byte) (x9 >> 24);
+
+ out[40] = (byte) (x10);
+ out[41] = (byte) (x10 >> 8);
+ out[42] = (byte) (x10 >> 16);
+ out[43] = (byte) (x10 >> 24);
+
+ out[44] = (byte) (x11);
+ out[45] = (byte) (x11 >> 8);
+ out[46] = (byte) (x11 >> 16);
+ out[47] = (byte) (x11 >> 24);
+
+ out[48] = (byte) (x12);
+ out[49] = (byte) (x12 >> 8);
+ out[50] = (byte) (x12 >> 16);
+ out[51] = (byte) (x12 >> 24);
+
+ out[52] = (byte) (x13);
+ out[53] = (byte) (x13 >> 8);
+ out[54] = (byte) (x13 >> 16);
+ out[55] = (byte) (x13 >> 24);
+
+ out[56] = (byte) (x14);
+ out[57] = (byte) (x14 >> 8);
+ out[58] = (byte) (x14 >> 16);
+ out[59] = (byte) (x14 >> 24);
+
+ out[60] = (byte) (x15);
+ out[61] = (byte) (x15 >> 8);
+ out[62] = (byte) (x15 >> 16);
+ out[63] = (byte) (x15 >> 24);
+ return out;
+ }
+
+ // XORKeyStream crypts bytes from in to out using the given key and counters.
+ // In and out may be the same slice but otherwise should not overlap. Counter
+ // contains the raw salsa20 counter bytes (both nonce and block counter).
+ public static byte[] XORKeyStream(byte[] in, byte[] counter, byte[] key) {
+ byte[] out = in.clone();
+ byte[] block;
+ byte[] counterCopy = counter.clone();
+
+ int count = 0;
+ while (in.length >= 64) {
+ block = core(counterCopy, key, SIGMA);
+
+ for (int i = 0; i < block.length; i++) {
+ byte x = block[i];
+ out[i + 64 * count] = (byte) (in[i] ^ x);
+ }
+ long u = 1;
+ for (int i = 8; i < 16; i++) {
+ u += 0xFF & counterCopy[i];
+ counterCopy[i] = (byte) (u);
+ u >>= 8;
+ }
+ byte[] temp = in.clone();
+ in = new byte[in.length - 64];
+ System.arraycopy(temp, 64, in, 0, in.length);
+
+ count++;
+ }
+
+ if (in.length > 0) {
+ block = core(counterCopy, key, SIGMA);
+
+ for (int i = 0; i < in.length; i++) {
+ out[i + count * 64] = (byte) (in[i] ^ block[i]);
+ }
+ }
+
+ return out;
+ }
+
+ // HSalsa20 applies the HSalsa20 core function to a 16-byte input in, 32-byte
+ // key k, and 16-byte constant c, and returns the result as the 32-byte array
+ // out.
+ public static byte[] HSalsa20(byte[] in, byte[] k, byte[] c) {
+ long x0 = mask(c[0]) | mask(c[1]) << 8 | mask(c[2]) << 16 | mask(c[3]) << 24;
+ long x1 = mask(k[0]) | mask(k[1]) << 8 | mask(k[2]) << 16 | mask(k[3]) << 24;
+ long x2 = mask(k[4]) | mask(k[5]) << 8 | mask(k[6]) << 16 | mask(k[7]) << 24;
+ long x3 = mask(k[8]) | mask(k[9]) << 8 | mask(k[10]) << 16 | mask(k[11]) << 24;
+ long x4 = mask(k[12]) | mask(k[13]) << 8 | mask(k[14]) << 16 | mask(k[15]) << 24;
+ long x5 = mask(c[4]) | mask(c[5]) << 8 | mask(c[6]) << 16 | mask(c[7]) << 24;
+ long x6 = mask(in[0]) | mask(in[1]) << 8 | mask(in[2]) << 16 | mask(in[3]) << 24;
+ long x7 = mask(in[4]) | mask(in[5]) << 8 | mask(in[6]) << 16 | mask(in[7]) << 24;
+ long x8 = mask(in[8]) | mask(in[9]) << 8 | mask(in[10]) << 16 | mask(in[11]) << 24;
+ long x9 = mask(in[12]) | mask(in[13]) << 8 | mask(in[14]) << 16 | mask(in[15]) << 24;
+ long x10 = mask(c[8]) | mask(c[9]) << 8 | mask(c[10]) << 16 | mask(c[11]) << 24;
+ long x11 = mask(k[16]) | mask(k[17]) << 8 | mask(k[18]) << 16 | mask(k[19]) << 24;
+ long x12 = mask(k[20]) | mask(k[21]) << 8 | mask(k[22]) << 16 | mask(k[23]) << 24;
+ long x13 = mask(k[24]) | mask(k[25]) << 8 | mask(k[26]) << 16 | mask(k[27]) << 24;
+ long x14 = mask(k[28]) | mask(k[29]) << 8 | mask(k[30]) << 16 | mask(k[31]) << 24;
+ long x15 = mask(c[12]) | mask(c[13]) << 8 | mask(c[14]) << 16 | mask(c[15]) << 24;
+
+ long mask = 0xFFFFFFFFL;
+ for (int i = 0; i < 20; i += 2) {
+ long u = mask & (x0 + x12);
+ x4 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x4 + x0);
+ x8 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x8 + x4);
+ x12 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x12 + x8);
+ x0 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x5 + x1);
+ x9 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x9 + x5);
+ x13 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x13 + x9);
+ x1 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x1 + x13);
+ x5 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x10 + x6);
+ x14 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x14 + x10);
+ x2 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x2 + x14);
+ x6 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x6 + x2);
+ x10 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x15 + x11);
+ x3 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x3 + x15);
+ x7 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x7 + x3);
+ x11 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x11 + x7);
+ x15 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x0 + x3);
+ x1 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x1 + x0);
+ x2 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x2 + x1);
+ x3 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x3 + x2);
+ x0 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x5 + x4);
+ x6 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x6 + x5);
+ x7 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x7 + x6);
+ x4 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x4 + x7);
+ x5 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x10 + x9);
+ x11 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x11 + x10);
+ x8 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x8 + x11);
+ x9 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x9 + x8);
+ x10 ^= mask & (u << 18 | u >>> (32 - 18));
+
+ u = mask & (x15 + x14);
+ x12 ^= mask & (u << 7 | u >>> (32 - 7));
+ u = mask & (x12 + x15);
+ x13 ^= mask & (u << 9 | u >>> (32 - 9));
+ u = mask & (x13 + x12);
+ x14 ^= mask & (u << 13 | u >>> (32 - 13));
+ u = mask & (x14 + x13);
+ x15 ^= mask & (u << 18 | u >>> (32 - 18));
+ }
+
+ byte[] out = new byte[32];
+ out[0] = (byte) x0;
+ out[1] = (byte) (x0 >> 8);
+ out[2] = (byte) (x0 >> 16);
+ out[3] = (byte) (x0 >> 24);
+
+ out[4] = (byte) (x5);
+ out[5] = (byte) (x5 >> 8);
+ out[6] = (byte) (x5 >> 16);
+ out[7] = (byte) (x5 >> 24);
+
+ out[8] = (byte) (x10);
+ out[9] = (byte) (x10 >> 8);
+ out[10] = (byte) (x10 >> 16);
+ out[11] = (byte) (x10 >> 24);
+
+ out[12] = (byte) (x15);
+ out[13] = (byte) (x15 >> 8);
+ out[14] = (byte) (x15 >> 16);
+ out[15] = (byte) (x15 >> 24);
+
+ out[16] = (byte) (x6);
+ out[17] = (byte) (x6 >> 8);
+ out[18] = (byte) (x6 >> 16);
+ out[19] = (byte) (x6 >> 24);
+
+ out[20] = (byte) (x7);
+ out[21] = (byte) (x7 >> 8);
+ out[22] = (byte) (x7 >> 16);
+ out[23] = (byte) (x7 >> 24);
+
+ out[24] = (byte) (x8);
+ out[25] = (byte) (x8 >> 8);
+ out[26] = (byte) (x8 >> 16);
+ out[27] = (byte) (x8 >> 24);
+
+ out[28] = (byte) (x9);
+ out[29] = (byte) (x9 >> 8);
+ out[30] = (byte) (x9 >> 16);
+ out[31] = (byte) (x9 >> 24);
+ return out;
+ }
+}
diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java
new file mode 100644
index 00000000..85ad6fe4
--- /dev/null
+++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpener.java
@@ -0,0 +1,118 @@
+/*
+Copyright 2020 Pusher Ltd
+Copyright 2015 Eve Freeman
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+*/
+
+package com.pusher.client.crypto.nacl;
+
+import static com.pusher.client.util.internal.Preconditions.checkArgument;
+import static com.pusher.client.util.internal.Preconditions.checkNotNull;
+import static java.util.Arrays.fill;
+
+public class SecretBoxOpener {
+
+ private static final int OVERHEAD = Poly1305.TAG_SIZE;
+
+ private byte[] key;
+
+ public SecretBoxOpener(byte[] key) {
+ checkNotNull(key, "null key passed");
+ checkArgument(key.length == 32, "key length must be 32 bytes, but is " +
+ key.length + " bytes");
+
+ this.key = key;
+ }
+
+ public byte[] open(byte[] box, byte[] nonce) throws AuthenticityException {
+ checkNotNull(key, "key has been cleared, create new instance");
+ checkArgument(nonce.length == 24, "nonce length must be 24 bytes, but is " +
+ key.length + " bytes");
+
+ byte[] subKey = new byte[32];
+ byte[] counter = new byte[16];
+ setup(subKey, counter, nonce, key);
+
+ // The Poly1305 key is generated by encrypting 32 bytes of zeros. Since
+ // Salsa20 works with 64-byte blocks, we also generate 32 bytes of
+ // keystream as a side effect.
+ byte[] firstBlock = new byte[64];
+ firstBlock = Salsa.XORKeyStream(firstBlock, counter, subKey);
+
+ byte[] poly1305Key = new byte[32];
+ System.arraycopy(firstBlock, 0, poly1305Key, 0, poly1305Key.length);
+ byte[] tag = new byte[Poly1305.TAG_SIZE];
+ System.arraycopy(box, 0, tag, 0, tag.length);
+
+ byte[] cipher = new byte[box.length - Poly1305.TAG_SIZE];
+ System.arraycopy(box, Poly1305.TAG_SIZE, cipher, 0, cipher.length);
+ if (!Poly1305.verify(tag, cipher, poly1305Key)) {
+ throw new AuthenticityException();
+ }
+
+ byte[] ret = new byte[box.length - OVERHEAD];
+ System.arraycopy(box, OVERHEAD, ret, 0, ret.length);
+ // We XOR up to 32 bytes of box with the keystream generated from
+ // the first block.
+ byte[] firstMessageBlock = new byte[ret.length];
+ if (ret.length > 32) {
+ firstMessageBlock = new byte[32];
+ }
+ System.arraycopy(ret, 0, firstMessageBlock, 0, firstMessageBlock.length);
+ for (int i = 0; i < firstMessageBlock.length; i++) {
+ ret[i] = (byte) (firstBlock[32 + i] ^ firstMessageBlock[i]);
+ }
+
+ counter[8] = 1;
+ byte[] newbox = new byte[box.length - (firstMessageBlock.length + OVERHEAD)];
+ for (int i = 0; i < newbox.length; i++) {
+ newbox[i] = box[i + firstMessageBlock.length + OVERHEAD];
+ }
+ byte[] rest = Salsa.XORKeyStream(newbox, counter, subKey);
+ // Now decrypt the rest.
+
+ System.arraycopy(rest, 0, ret, firstMessageBlock.length,
+ ret.length - firstMessageBlock.length);
+
+ return ret;
+ }
+
+ public void clearKey() {
+ fill(key, (byte) 0);
+ if (key[0] != 0) {
+ // so that hopefully the optimiser won't remove the clearing code (best sensible effort)
+ throw new SecurityException("key not cleared correctly");
+ }
+ key = null;
+ }
+
+ // subKey = byte[32], counter = byte[16], nonce = byte[24], key = byte[32]
+ private void setup(byte[] subKey, byte[] counter, byte[] nonce, byte[] key) {
+ // We use XSalsa20 for encryption so first we need to generate a
+ // key and nonce with HSalsa20.
+ byte[] hNonce = new byte[16];
+ System.arraycopy(nonce, 0, hNonce, 0, hNonce.length);
+ byte[] newSubKey = Salsa.HSalsa20(hNonce, key, Salsa.SIGMA);
+ System.arraycopy(newSubKey, 0, subKey, 0, subKey.length);
+
+ System.arraycopy(nonce, 16, counter, 0, nonce.length - 16);
+ }
+}
diff --git a/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpenerFactory.java b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpenerFactory.java
new file mode 100644
index 00000000..ea644c85
--- /dev/null
+++ b/src/main/java/com/pusher/client/crypto/nacl/SecretBoxOpenerFactory.java
@@ -0,0 +1,8 @@
+package com.pusher.client.crypto.nacl;
+
+public class SecretBoxOpenerFactory {
+
+ public SecretBoxOpener create(byte[] key) {
+ return new SecretBoxOpener(key);
+ }
+}
diff --git a/src/main/java/com/pusher/client/crypto/nacl/Subtle.java b/src/main/java/com/pusher/client/crypto/nacl/Subtle.java
new file mode 100644
index 00000000..fbe2bf6f
--- /dev/null
+++ b/src/main/java/com/pusher/client/crypto/nacl/Subtle.java
@@ -0,0 +1,45 @@
+/*
+Copyright 2015 Eve Freeman
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+*/
+
+package com.pusher.client.crypto.nacl;
+
+public class Subtle {
+ public static boolean constantTimeCompare(byte[] x, byte[] y) {
+ if (x.length != y.length) {
+ return false;
+ }
+ byte v = 0;
+ for (int i = 0; i < x.length; i++) {
+ v |= x[i] ^ y[i];
+ }
+ return constantTimeByteEq(v, (byte) 0);
+ }
+
+ public static boolean constantTimeByteEq(byte x, byte y) {
+ byte z = (byte) ~(x ^ y);
+ z &= (byte) (z >> 4);
+ z &= (byte) (z >> 2);
+ z &= (byte) (z >> 1);
+ return z == -1;
+ }
+}
diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java
new file mode 100644
index 00000000..38acdde8
--- /dev/null
+++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java
@@ -0,0 +1,114 @@
+package com.pusher.client.example;
+
+import com.pusher.client.Pusher;
+import com.pusher.client.PusherOptions;
+import com.pusher.client.channel.PrivateEncryptedChannel;
+import com.pusher.client.channel.PrivateEncryptedChannelEventListener;
+import com.pusher.client.channel.PusherEvent;
+import com.pusher.client.connection.ConnectionEventListener;
+import com.pusher.client.connection.ConnectionStateChange;
+import com.pusher.client.util.HttpAuthorizer;
+
+/*
+This app demonstrates how to use Private Encrypted Channels [BETA].
+
+Please ensure you update this relevant parts below with your Pusher credentials before running.
+and ensure you have set up an authorization endpoint with end to end encryption. Your Pusher credentials
+can be found at https://dashboard.pusher.com, selecting the channels project, and visiting the App Keys
+tab.
+
+A demonstration authorization endpoint using nodejs can be found
+https://github.com/pusher/pusher-channels-auth-example#using-e2e-encryption
+
+For more information on private encrypted channels please read
+https://pusher.com/docs/channels/using_channels/encrypted-channels
+
+For more pecific information on how to use private encrypted channels check out
+https://github.com/pusher/pusher-websocket-java#private-encrypted-channels
+ */
+
+public class PrivateEncryptedChannelExampleApp implements
+ ConnectionEventListener, PrivateEncryptedChannelEventListener {
+
+ private String channelsKey = "FILL_ME_IN";
+ private String channelName = "private-encrypted-channel";
+ private String eventName = "my-event";
+ private String cluster = "eu";
+ private String authorizationEndpoint = "http://localhost:3030/pusher/auth";
+
+ private PrivateEncryptedChannel channel;
+
+ public static void main(final String[] args) {
+ new PrivateEncryptedChannelExampleApp(args);
+ }
+
+ private PrivateEncryptedChannelExampleApp(final String[] args) {
+ switch (args.length) {
+ case 4: cluster = args[3];
+ case 3: eventName = args[2];
+ case 2: channelName = args[1];
+ case 1: channelsKey = args[0];
+ }
+
+ final HttpAuthorizer authorizer = new HttpAuthorizer(
+ authorizationEndpoint);
+ final PusherOptions options = new PusherOptions().setAuthorizer(authorizer).setEncrypted(true);
+ options.setCluster(cluster);
+
+ Pusher pusher = new Pusher(channelsKey, options);
+ pusher.connect(this);
+
+ channel = pusher.subscribePrivateEncrypted(channelName, this, eventName);
+
+ // Keep main thread asleep while we watch for events or application will terminate
+ while (true) {
+ try {
+ Thread.sleep(1000);
+ }
+ catch (final InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onAuthenticationFailure(String message, Exception e) {
+ System.out.println(String.format(
+ "Authentication failure due to [%s], exception was [%s]", message, e));
+ }
+
+ @Override
+ public void onSubscriptionSucceeded(String channelName) {
+ System.out.println(String.format(
+ "Subscription to channel [%s] succeeded", channel.getName()));
+ }
+
+ @Override
+ public void onEvent(PusherEvent event) {
+ System.out.println(String.format(
+ "Received event [%s]", event.toString()));
+ }
+
+ @Override
+ public void onConnectionStateChange(ConnectionStateChange change) {
+ System.out.println(String.format(
+ "Connection state changed from [%s] to [%s]",
+ change.getPreviousState(),
+ change.getCurrentState()));
+ }
+
+ @Override
+ public void onError(String message, String code, Exception e) {
+ System.out.println(String.format(
+ "An error was received with message [%s], code [%s], exception [%s]",
+ message,
+ code,
+ e));
+ }
+
+ @Override
+ public void onDecryptionFailure(String event, String reason) {
+ System.out.println(String.format(
+ "An error was received decrypting message for event:[%s] - reason: [%s]", event, reason));
+ }
+}
diff --git a/src/main/java/com/pusher/client/util/Factory.java b/src/main/java/com/pusher/client/util/Factory.java
index b7622c78..fd296380 100644
--- a/src/main/java/com/pusher/client/util/Factory.java
+++ b/src/main/java/com/pusher/client/util/Factory.java
@@ -14,8 +14,10 @@
import com.pusher.client.PusherOptions;
import com.pusher.client.channel.impl.ChannelImpl;
import com.pusher.client.channel.impl.ChannelManager;
+import com.pusher.client.channel.impl.PrivateEncryptedChannelImpl;
import com.pusher.client.channel.impl.PresenceChannelImpl;
import com.pusher.client.channel.impl.PrivateChannelImpl;
+import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory;
import com.pusher.client.connection.impl.InternalConnection;
import com.pusher.client.connection.websocket.WebSocketClientWrapper;
import com.pusher.client.connection.websocket.WebSocketConnection;
@@ -86,6 +88,14 @@ public PrivateChannelImpl newPrivateChannel(final InternalConnection connection,
return new PrivateChannelImpl(connection, channelName, authorizer, this);
}
+ public PrivateEncryptedChannelImpl newPrivateEncryptedChannel(
+ final InternalConnection connection,
+ final String channelName,
+ final Authorizer authorizer) {
+ return new PrivateEncryptedChannelImpl(connection, channelName, authorizer, this,
+ new SecretBoxOpenerFactory());
+ }
+
public PresenceChannelImpl newPresenceChannel(final InternalConnection connection, final String channelName,
final Authorizer authorizer) {
return new PresenceChannelImpl(connection, channelName, authorizer, this);
diff --git a/src/main/java/com/pusher/client/util/internal/Base64.java b/src/main/java/com/pusher/client/util/internal/Base64.java
new file mode 100644
index 00000000..24b506aa
--- /dev/null
+++ b/src/main/java/com/pusher/client/util/internal/Base64.java
@@ -0,0 +1,48 @@
+package com.pusher.client.util.internal;
+
+import static java.util.Arrays.fill;
+
+// copied from: https://stackoverflow.com/a/4265472/501940 and improved (naming, char validation)
+public class Base64 {
+
+ private final static char[] CHAR_INDEX_TABLE =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
+
+ private static int[] charToIndexSparseMappingArray = new int[128];
+
+ static {
+ fill(charToIndexSparseMappingArray, -1);
+ for (int i = 0; i < CHAR_INDEX_TABLE.length; i++) {
+ charToIndexSparseMappingArray[CHAR_INDEX_TABLE[i]] = i;
+ }
+ }
+
+ private static int toInt(char character) {
+ int retVal = charToIndexSparseMappingArray[character];
+ if (retVal == -1) throw new IllegalArgumentException("invalid char: " + character);
+ return retVal;
+ }
+
+ public static byte[] decode(String base64String) {
+ int paddingSize = base64String.endsWith("==") ? 2 : base64String.endsWith("=") ? 1 : 0;
+ byte[] retVal = new byte[base64String.length() * 3 / 4 - paddingSize];
+ int mask = 0xFF;
+ int index = 0;
+ for (int i = 0; i < base64String.length(); i += 4) {
+ int c0 = toInt(base64String.charAt(i));
+ int c1 = toInt(base64String.charAt(i + 1));
+ retVal[index++] = (byte) (((c0 << 2) | (c1 >> 4)) & mask);
+ if (index >= retVal.length) {
+ return retVal;
+ }
+ int c2 = toInt(base64String.charAt(i + 2));
+ retVal[index++] = (byte) (((c1 << 4) | (c2 >> 2)) & mask);
+ if (index >= retVal.length) {
+ return retVal;
+ }
+ int c3 = toInt(base64String.charAt(i + 3));
+ retVal[index++] = (byte) (((c2 << 6) | c3) & mask);
+ }
+ return retVal;
+ }
+}
diff --git a/src/main/java/com/pusher/client/util/internal/Preconditions.java b/src/main/java/com/pusher/client/util/internal/Preconditions.java
new file mode 100644
index 00000000..9841efe4
--- /dev/null
+++ b/src/main/java/com/pusher/client/util/internal/Preconditions.java
@@ -0,0 +1,23 @@
+package com.pusher.client.util.internal;
+
+public class Preconditions {
+
+ public static void checkArgument(boolean expression, Object errorMessage) {
+ if (!expression) {
+ throw new IllegalArgumentException(String.valueOf(errorMessage));
+ }
+ }
+
+ public static void checkState(boolean expression, Object errorMessage) {
+ if (!expression) {
+ throw new IllegalStateException(String.valueOf(errorMessage));
+ }
+ }
+
+ public static T checkNotNull(T reference, Object errorMessage) {
+ if (reference == null) {
+ throw new NullPointerException(String.valueOf(errorMessage));
+ }
+ return reference;
+ }
+}
diff --git a/src/test/java/com/pusher/client/channel/impl/ChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/ChannelImplTest.java
index 917d3e41..894c2c01 100644
--- a/src/test/java/com/pusher/client/channel/impl/ChannelImplTest.java
+++ b/src/test/java/com/pusher/client/channel/impl/ChannelImplTest.java
@@ -57,6 +57,11 @@ public void testPrivateChannelName() {
newInstance("private-my-channel");
}
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrivateEncryptedChannelName() {
+ newInstance("private-encrypted-my-channel");
+ }
+
@Test(expected = IllegalArgumentException.class)
public void testPresenceChannelName() {
newInstance("presence-my-channel");
diff --git a/src/test/java/com/pusher/client/channel/impl/PresenceChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PresenceChannelImplTest.java
index 177e7367..a9c3f96e 100644
--- a/src/test/java/com/pusher/client/channel/impl/PresenceChannelImplTest.java
+++ b/src/test/java/com/pusher/client/channel/impl/PresenceChannelImplTest.java
@@ -214,6 +214,12 @@ public void testPrivateChannelName() {
newInstance("private-stuffchannel");
}
+ @Override
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrivateEncryptedChannelName() {
+ newInstance("private-encrypted-stuffchannel");
+ }
+
@Override
@Test
public void testPresenceChannelName() {
diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateChannelImplTest.java
index 1bf0e6e7..16747677 100644
--- a/src/test/java/com/pusher/client/channel/impl/PrivateChannelImplTest.java
+++ b/src/test/java/com/pusher/client/channel/impl/PrivateChannelImplTest.java
@@ -64,6 +64,12 @@ public void testPresenceChannelName() {
newInstance("presence-stuffchannel");
}
+ @Override
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrivateEncryptedChannelName() {
+ newInstance("private-encrypted-stuffchannel");
+ }
+
@Override
@Test
public void testPrivateChannelName() {
diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java
new file mode 100644
index 00000000..19521a7d
--- /dev/null
+++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelClearsKeyTest.java
@@ -0,0 +1,92 @@
+package com.pusher.client.channel.impl;
+
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import com.pusher.client.Authorizer;
+import com.pusher.client.channel.ChannelState;
+import com.pusher.client.connection.ConnectionEventListener;
+import com.pusher.client.connection.ConnectionState;
+import com.pusher.client.connection.ConnectionStateChange;
+import com.pusher.client.connection.impl.InternalConnection;
+import com.pusher.client.crypto.nacl.SecretBoxOpener;
+import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory;
+import com.pusher.client.util.Factory;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.mockito.stubbing.Answer;
+
+@RunWith(MockitoJUnitRunner.class)
+public class PrivateEncryptedChannelClearsKeyTest {
+
+ final String CHANNEL_NAME = "private-encrypted-unit-test-channel";
+ final String AUTH_RESPONSE = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\",\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}";
+
+ @Mock
+ InternalConnection mockInternalConnection;
+ @Mock
+ Authorizer mockAuthorizer;
+ @Mock
+ Factory mockFactory;
+
+ @Mock
+ SecretBoxOpenerFactory mockSecretBoxOpenerFactory;
+ @Mock
+ SecretBoxOpener mockSecretBoxOpener;
+
+ PrivateEncryptedChannelImpl subject;
+
+ @Before
+ public void setUp() {
+ when(mockAuthorizer.authorize(eq(CHANNEL_NAME), anyString())).thenReturn(AUTH_RESPONSE);
+ when(mockSecretBoxOpenerFactory.create(any())).thenReturn(mockSecretBoxOpener);
+
+ subject = new PrivateEncryptedChannelImpl(mockInternalConnection, CHANNEL_NAME,
+ mockAuthorizer, mockFactory, mockSecretBoxOpenerFactory);
+ }
+
+ @Test
+ public void secretBoxOpenerIsClearedOnUnsubscribed() {
+ subject.toSubscribeMessage();
+
+ subject.updateState(ChannelState.UNSUBSCRIBED);
+
+ verify(mockSecretBoxOpener).clearKey();
+ }
+
+ @Test
+ public void secretBoxOpenerIsClearedOnDisconnected() {
+ doAnswer((Answer) invocation -> {
+ ConnectionEventListener l = (ConnectionEventListener) invocation.getArguments()[1];
+ l.onConnectionStateChange(new ConnectionStateChange(
+ ConnectionState.DISCONNECTING,
+ ConnectionState.DISCONNECTED
+ ));
+ return null;
+ }).when(mockInternalConnection).bind(eq(ConnectionState.DISCONNECTED), any());
+ subject.toSubscribeMessage();
+
+ verify(mockSecretBoxOpener).clearKey();
+ }
+
+ @Test
+ public void secretBoxOpenerIsClearedOnceOnUnsubscribedAndThenDisconnected() {
+ doAnswer((Answer) invocation -> {
+ subject.updateState(ChannelState.UNSUBSCRIBED);
+
+ ConnectionEventListener l = (ConnectionEventListener) invocation.getArguments()[1];
+ l.onConnectionStateChange(new ConnectionStateChange(
+ ConnectionState.DISCONNECTING,
+ ConnectionState.DISCONNECTED
+ ));
+
+ return null;
+ }).when(mockInternalConnection).bind(eq(ConnectionState.DISCONNECTED), any());
+ subject.toSubscribeMessage();
+
+ verify(mockSecretBoxOpener).clearKey();
+ }
+}
diff --git a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java
new file mode 100644
index 00000000..c11eb6ce
--- /dev/null
+++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java
@@ -0,0 +1,364 @@
+package com.pusher.client.channel.impl;
+
+import com.pusher.client.AuthorizationFailureException;
+import com.pusher.client.Authorizer;
+import com.pusher.client.channel.ChannelEventListener;
+import com.pusher.client.channel.PrivateEncryptedChannelEventListener;
+import com.pusher.client.connection.impl.InternalConnection;
+import com.pusher.client.crypto.nacl.SecretBoxOpener;
+import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory;
+import com.pusher.client.util.internal.Base64;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class PrivateEncryptedChannelImplTest extends ChannelImplTest {
+
+ final String AUTH_RESPONSE = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\",\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}";
+ final String AUTH_RESPONSE_MISSING_AUTH = "{\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}";
+ final String AUTH_RESPONSE_MISSING_SHARED_SECRET = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\"}";
+ final String AUTH_RESPONSE_INVALID_JSON = "potatoes";
+ final String SHARED_SECRET = "iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=";
+ final String AUTH_RESPONSE_INCORRECT_SHARED_SECRET = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\",\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5do0=\"}";
+ final String SHARED_SECRET_INCORRECT = "iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5do0=";
+
+ @Mock
+ InternalConnection mockInternalConnection;
+ @Mock
+ Authorizer mockAuthorizer;
+ @Mock
+ SecretBoxOpenerFactory mockSecretBoxOpenerFactory;
+
+ @Override
+ @Before
+ public void setUp() {
+ super.setUp();
+ when(mockAuthorizer.authorize(eq(getChannelName()), anyString())).thenReturn(AUTH_RESPONSE);
+ }
+
+ protected PrivateEncryptedChannelImpl newInstance() {
+ return new PrivateEncryptedChannelImpl(mockInternalConnection, getChannelName(),
+ mockAuthorizer, factory, mockSecretBoxOpenerFactory);
+ }
+
+ @Override
+ protected ChannelImpl newInstance(final String channelName) {
+ return new PrivateEncryptedChannelImpl(mockInternalConnection, channelName, mockAuthorizer,
+ factory, mockSecretBoxOpenerFactory);
+ }
+
+ protected String getChannelName() {
+ return "private-encrypted-channel";
+ }
+
+ @Test
+ public void toStringIsAccurate() {
+ assertEquals("[Private Encrypted Channel: name="+getChannelName()+"]", channel.toString());
+ }
+
+
+ /*
+ TESTING VALID PRIVATE ENCRYPTED CHANNEL NAMES
+ */
+
+ @Override
+ @Test(expected = IllegalArgumentException.class)
+ public void testPublicChannelName() {
+ newInstance("stuffchannel");
+ }
+
+ @Override
+ @Test(expected = IllegalArgumentException.class)
+ public void testPresenceChannelName() {
+ newInstance("presence-stuffchannel");
+ }
+
+ @Override
+ @Test
+ public void testPrivateEncryptedChannelName() {
+ newInstance("private-encrypted-stuffchannel");
+ }
+
+ @Override
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrivateChannelName() { newInstance("private-stuffchannel"); }
+
+ /*
+ TESTING SUBSCRIBE MESSAGE
+ */
+
+ @Override
+ @Test
+ public void testReturnsCorrectSubscribeMessage() {
+ assertEquals("{\"event\":\"pusher:subscribe\",\"data\":{" +
+ "\"channel\":\"" + getChannelName() + "\"," +
+ "\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\""+
+ "}}", channel.toSubscribeMessage());
+ }
+
+ /*
+ TESTING AUTHENTICATION METHOD
+ */
+
+ @Test
+ public void authenticationSucceedsGivenValidAuthorizer() {
+ when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString()))
+ .thenReturn(AUTH_RESPONSE);
+
+ PrivateEncryptedChannelImpl channel = newInstance();
+
+ channel.toSubscribeMessage();
+ }
+
+ protected ChannelEventListener getEventListener() {
+ return mock(PrivateEncryptedChannelEventListener.class);
+ }
+
+ @Test(expected = AuthorizationFailureException.class)
+ public void authenticationThrowsExceptionIfNoAuthKey() {
+ when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString()))
+ .thenReturn(AUTH_RESPONSE_MISSING_AUTH);
+
+ PrivateEncryptedChannelImpl channel = newInstance();
+
+ channel.toSubscribeMessage();
+ }
+
+ @Test(expected = AuthorizationFailureException.class)
+ public void authenticationThrowsExceptionIfNoSharedSecret() {
+ when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString()))
+ .thenReturn(AUTH_RESPONSE_MISSING_SHARED_SECRET);
+
+ PrivateEncryptedChannelImpl channel = newInstance();
+
+ channel.toSubscribeMessage();
+ }
+
+ @Test(expected = AuthorizationFailureException.class)
+ public void authenticationThrowsExceptionIfMalformedJson() {
+ when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString()))
+ .thenReturn(AUTH_RESPONSE_INVALID_JSON);
+
+ PrivateEncryptedChannelImpl channel = newInstance();
+
+ channel.toSubscribeMessage();
+ }
+
+ /*
+ ON MESSAGE
+ */
+ @Test
+ public void testDataIsExtractedFromMessageAndPassedToSingleListener() {
+ PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl(
+ mockInternalConnection,
+ getChannelName(),
+ mockAuthorizer,
+ factory,
+ mockSecretBoxOpenerFactory);
+
+ when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString()))
+ .thenReturn(AUTH_RESPONSE);
+ when(mockSecretBoxOpenerFactory.create(any()))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET)));
+
+ channel.toSubscribeMessage();
+
+ PrivateEncryptedChannelEventListener mockListener = mock(PrivateEncryptedChannelEventListener.class);
+
+ channel.bind("my-event", mockListener);
+ channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" +
+ "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," +
+ "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" +
+ "}\"}");
+
+ verify(mockListener, times(1)).onEvent(argCaptor.capture());
+ assertEquals("event1", argCaptor.getValue().getEventName());
+ assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData());
+ }
+
+ @Test
+ public void testDataIsExtractedFromMessageAndPassedToMultipleListeners() {
+ PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl(
+ mockInternalConnection,
+ getChannelName(),
+ mockAuthorizer,
+ factory,
+ mockSecretBoxOpenerFactory);
+
+ when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString()))
+ .thenReturn(AUTH_RESPONSE);
+ when(mockSecretBoxOpenerFactory.create(any()))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET)));
+
+ channel.toSubscribeMessage();
+
+ PrivateEncryptedChannelEventListener mockListener1 = mock(PrivateEncryptedChannelEventListener.class);
+ PrivateEncryptedChannelEventListener mockListener2 = mock(PrivateEncryptedChannelEventListener.class);
+
+ channel.bind("my-event", mockListener1);
+ channel.bind("my-event", mockListener2);
+ channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" +
+ "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," +
+ "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" +
+ "}\"}");
+
+ verify(mockListener1).onEvent(argCaptor.capture());
+ assertEquals("event1", argCaptor.getValue().getEventName());
+ assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData());
+
+ verify(mockListener2).onEvent(argCaptor.capture());
+ assertEquals("event1", argCaptor.getValue().getEventName());
+ assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData());
+ }
+
+ @Test
+ public void onMessageRaisesExceptionWhenFailingToDecryptTwice() {
+ PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl(
+ mockInternalConnection,
+ getChannelName(),
+ mockAuthorizer,
+ factory,
+ mockSecretBoxOpenerFactory);
+
+ when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString()))
+ .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET)
+ .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET);
+ when(mockSecretBoxOpenerFactory.create(any()))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT)))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT)));
+
+ channel.toSubscribeMessage();
+
+ PrivateEncryptedChannelEventListener mockListener1 = mock(PrivateEncryptedChannelEventListener.class);
+ channel.bind("my-event", mockListener1);
+ channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" +
+ "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," +
+ "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" +
+ "}\"}");
+
+ verify(mockListener1).onDecryptionFailure(anyString(), anyString());
+ }
+
+ @Test
+ public void onMessageRetriesDecryptionOnce() {
+ PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl(
+ mockInternalConnection,
+ getChannelName(),
+ mockAuthorizer,
+ factory,
+ mockSecretBoxOpenerFactory);
+
+ when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString()))
+ .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET)
+ .thenReturn(AUTH_RESPONSE);
+ when(mockSecretBoxOpenerFactory.create(any()))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT)))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET)));
+
+ channel.toSubscribeMessage();
+
+ PrivateEncryptedChannelEventListener mockListener1 = mock(PrivateEncryptedChannelEventListener.class);
+ channel.bind("my-event", mockListener1);
+ channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" +
+ "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," +
+ "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" +
+ "}\"}");
+
+ verify(mockListener1).onEvent(argCaptor.capture());
+ assertEquals("event1", argCaptor.getValue().getEventName());
+ assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData());
+ }
+
+ @Test
+ public void twoEventsReceivedWithSecondRetryCorrect() {
+
+ PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl(
+ mockInternalConnection,
+ getChannelName(),
+ mockAuthorizer,
+ factory,
+ mockSecretBoxOpenerFactory);
+
+ when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString()))
+ .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET)
+ .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET)
+ .thenReturn(AUTH_RESPONSE);
+ when(mockSecretBoxOpenerFactory.create(any()))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT)))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT)))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET)));
+
+ channel.toSubscribeMessage();
+
+ PrivateEncryptedChannelEventListener mockListener1 = mock(PrivateEncryptedChannelEventListener.class);
+ channel.bind("my-event", mockListener1);
+ channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" +
+ "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," +
+ "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" +
+ "}\"}");
+
+ verify(mockListener1).onDecryptionFailure("my-event", "Failed to decrypt message.");
+
+ // send a second message
+ channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" +
+ "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," +
+ "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" +
+ "}\"}");
+
+ verify(mockListener1).onEvent(argCaptor.capture());
+ assertEquals("event1", argCaptor.getValue().getEventName());
+ assertEquals("{\"message\":\"hello world\"}", argCaptor.getValue().getData());
+ }
+
+ @Test
+ public void twoEventsReceivedWithIncorrectSharedSecret() {
+
+ PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl(
+ mockInternalConnection,
+ getChannelName(),
+ mockAuthorizer,
+ factory,
+ mockSecretBoxOpenerFactory);
+
+ when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString()))
+ .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET)
+ .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET)
+ .thenReturn(AUTH_RESPONSE_INCORRECT_SHARED_SECRET);
+ when(mockSecretBoxOpenerFactory.create(any()))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT)))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT)))
+ .thenReturn(new SecretBoxOpener(Base64.decode(SHARED_SECRET_INCORRECT)));
+
+ channel.toSubscribeMessage();
+
+ PrivateEncryptedChannelEventListener mockListener1 = mock(PrivateEncryptedChannelEventListener.class);
+ channel.bind("my-event", mockListener1);
+ channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" +
+ "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," +
+ "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" +
+ "}\"}");
+ // send a second message
+ channel.onMessage("my-event", "{\"event\":\"event1\",\"data\":\"{" +
+ "\\\"nonce\\\": \\\"4sVYwy4j/8dCcjyxtPCWyk19GaaViaW9\\\"," +
+ "\\\"ciphertext\\\": \\\"/GMESnFGlbNn01BuBjp31XYa3i9vZsGKR8fgR9EDhXKx3lzGiUD501A=\\\"" +
+ "}\"}");
+
+ verify(mockListener1, times(2))
+ .onDecryptionFailure("my-event", "Failed to decrypt message.");
+ }
+
+}
diff --git a/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java
new file mode 100644
index 00000000..46077dc6
--- /dev/null
+++ b/src/test/java/com/pusher/client/crypto/nacl/SecretBoxOpenerTest.java
@@ -0,0 +1,45 @@
+package com.pusher.client.crypto.nacl;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.util.Arrays.copyOf;
+
+import com.pusher.client.util.internal.Base64;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SecretBoxOpenerTest {
+
+ byte[] key = Base64.decode("6071zp2l/GPnDPDXNWTJDHyIZ8pZMvQrYsa4xuTKK2c=");
+
+ byte[] cipher = Base64.decode("tvttPE2PRQp0bWDmaPyiEU8YJGztmTvTN77OoPwftTNTdDgJXwxHQPE=");
+ byte[] nonce = Base64.decode("xsbOS0KylAV2ziTDHrP/7rSFqpCOah3p");
+
+ SecretBoxOpener subject;
+
+ @Before
+ public void setUp() {
+ subject = new SecretBoxOpener(key);
+ }
+
+ @Test
+ public void open() {
+ byte[] clearText = subject.open(cipher, nonce);
+
+ assertThat(new String(clearText)).isEqualTo("{\"message\":\"hello world\"}");
+ }
+
+ @Test(expected = AuthenticityException.class)
+ public void openFailsForTamperedCipher() {
+ byte[] tamperedCipher = copyOf(cipher, cipher.length);
+ tamperedCipher[0] ^= tamperedCipher[0];
+
+ subject.open(tamperedCipher, nonce);
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void openFailsAfterClearKey() {
+ subject.clearKey();
+
+ subject.open(cipher, nonce);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/pusher/client/util/Base64Test.java b/src/test/java/com/pusher/client/util/Base64Test.java
new file mode 100644
index 00000000..0f027fa8
--- /dev/null
+++ b/src/test/java/com/pusher/client/util/Base64Test.java
@@ -0,0 +1,28 @@
+package com.pusher.client.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.pusher.client.util.internal.Base64;
+import org.junit.Test;
+
+public class Base64Test {
+
+ @Test
+ public void decodeValidChars() {
+ String validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ assertThat(Base64.decode(validChars)).isNotEmpty();
+ }
+
+ // https://en.wikipedia.org/wiki/Base64#URL_applications
+ @Test(expected = IllegalArgumentException.class)
+ public void failDecodingMinusChar() {
+ Base64.decode("-");
+ }
+
+ // https://en.wikipedia.org/wiki/Base64#URL_applications
+ @Test(expected = IllegalArgumentException.class)
+ public void failDecodingUnderscoreChar() {
+ Base64.decode("_");
+ }
+
+}