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 @@ [![Build Status](https://travis-ci.org/pusher/pusher-websocket-java.svg?branch=master)](https://travis-ci.org/pusher/pusher-websocket-java) [![codecov](https://codecov.io/gh/pusher/pusher-websocket-java/branch/master/graph/badge.svg)](https://codecov.io/gh/pusher/pusher-websocket-java) +[![Maven Central](https://img.shields.io/maven-central/v/com.pusher/pusher-java-client.svg?label=Maven%20Central)](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("_"); + } + +}