diff --git a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java index 05c46ba5..2d54cd85 100644 --- a/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java +++ b/src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java @@ -6,6 +6,9 @@ import com.pusher.client.channel.PrivateEncryptedChannel; import com.pusher.client.channel.PrivateEncryptedChannelEventListener; 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.SecretBoxOpener; import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory; @@ -22,6 +25,23 @@ public class PrivateEncryptedChannelImpl extends ChannelImpl implements PrivateE 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, @@ -45,22 +65,38 @@ public void bind(final String eventName, final SubscriptionEventListener listene super.bind(eventName, listener); } - private String authenticate() { + @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 { - final Map authResponseMap = GSON.fromJson(getAuthResponse(), Map.class); - final String auth = (String) authResponseMap.get("auth"); - final String sharedSecret = (String) authResponseMap.get("shared_secret"); + @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 { - secretBoxOpener = secretBoxOpenerFactory.create( - Base64.decode(sharedSecret)); + createSecretBoxOpener(Base64.decode(sharedSecret)); return auth; } - } catch (final AuthorizationFailureException e) { throw e; // pass this upwards } catch (final Exception e) { @@ -69,22 +105,14 @@ private String authenticate() { } } - @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); + private void createSecretBoxOpener(byte[] key) { + secretBoxOpener = secretBoxOpenerFactory.create(key); + setListenerToDisposeSecretBoxOpenerOnDisconnected(); + } - return GSON.toJson(jsonObject); + private void setListenerToDisposeSecretBoxOpenerOnDisconnected() { + connection.bind(ConnectionState.DISCONNECTED, + disposeSecretBoxOpenerOnDisconnectedListener); } @Override @@ -92,16 +120,23 @@ public void updateState(ChannelState state) { super.updateState(state); if (state == ChannelState.UNSUBSCRIBED) { - tearDownChannel(); + disposeSecretBoxOpener(); } } - private void tearDownChannel() { + 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); diff --git a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java index 340fdb57..b883e60f 100644 --- a/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java +++ b/src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java @@ -12,24 +12,23 @@ public class PrivateEncryptedChannelExampleApp implements ConnectionEventListener, PrivateEncryptedChannelEventListener { - private String apiKey = "FILL_ME_IN"; + private String apiKey = "FILL_ME_IN"; // "key" at https://dashboard.pusher.com private String channelName = "private-encrypted-channel"; private String eventName = "my-event"; private String cluster = "eu"; - private final PrivateEncryptedChannel channel; + private PrivateEncryptedChannel channel; public static void main(final String[] args) { new PrivateEncryptedChannelExampleApp(args); } private PrivateEncryptedChannelExampleApp(final String[] args) { - - if (args.length == 3) { - apiKey = args[0]; - channelName = args[1]; - eventName = args[2]; - cluster = args[3]; + switch (args.length) { + case 4: cluster = args[3]; + case 3: eventName = args[2]; + case 2: channelName = args[1]; + case 1: apiKey = args[0]; } final HttpAuthorizer authorizer = new HttpAuthorizer( 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 index 025872b5..d9e639bb 100644 --- a/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java +++ b/src/test/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImplTest.java @@ -1,15 +1,17 @@ package com.pusher.client.channel.impl; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import com.pusher.client.AuthorizationFailureException; import com.pusher.client.Authorizer; import com.pusher.client.channel.ChannelEventListener; -import com.pusher.client.channel.ChannelState; 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.Factory; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -17,42 +19,31 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -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.verify; -import static org.mockito.Mockito.when; - @RunWith(MockitoJUnitRunner.class) public class PrivateEncryptedChannelImplTest extends ChannelImplTest { - String authorizer_valid = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\",\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}"; - String authorizer_missingAuthKey = "{\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}"; - String authorizer_missingSharedSecret = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\"}"; - String authorizer_malformedJson = "potatoes"; - + 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"; @Mock InternalConnection mockInternalConnection; @Mock Authorizer mockAuthorizer; @Mock - Factory mockFactory; - @Mock SecretBoxOpenerFactory mockSecretBoxOpenerFactory; - @Mock - SecretBoxOpener mockSecretBoxOpener; - @Override @Before public void setUp() { super.setUp(); - when(mockAuthorizer.authorize(eq(getChannelName()), anyString())).thenReturn(authorizer_valid); + when(mockAuthorizer.authorize(eq(getChannelName()), anyString())).thenReturn(AUTH_RESPONSE); + } + + protected PrivateEncryptedChannelImpl newInstance() { + return new PrivateEncryptedChannelImpl(mockInternalConnection, getChannelName(), + mockAuthorizer, factory, mockSecretBoxOpenerFactory); } @Override @@ -117,11 +108,9 @@ public void testReturnsCorrectSubscribeMessage() { @Test public void authenticationSucceedsGivenValidAuthorizer() { when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) - .thenReturn(authorizer_valid); + .thenReturn(AUTH_RESPONSE); - PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( - mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, - mockSecretBoxOpenerFactory); + PrivateEncryptedChannelImpl channel = newInstance(); channel.toSubscribeMessage(); } @@ -133,11 +122,9 @@ protected ChannelEventListener getEventListener() { @Test(expected = AuthorizationFailureException.class) public void authenticationThrowsExceptionIfNoAuthKey() { when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) - .thenReturn(authorizer_missingAuthKey); + .thenReturn(AUTH_RESPONSE_MISSING_AUTH); - PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( - mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, - mockSecretBoxOpenerFactory); + PrivateEncryptedChannelImpl channel = newInstance(); channel.toSubscribeMessage(); } @@ -145,11 +132,9 @@ mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, @Test(expected = AuthorizationFailureException.class) public void authenticationThrowsExceptionIfNoSharedSecret() { when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) - .thenReturn(authorizer_missingSharedSecret); + .thenReturn(AUTH_RESPONSE_MISSING_SHARED_SECRET); - PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( - mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, - mockSecretBoxOpenerFactory); + PrivateEncryptedChannelImpl channel = newInstance(); channel.toSubscribeMessage(); } @@ -157,33 +142,10 @@ mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, @Test(expected = AuthorizationFailureException.class) public void authenticationThrowsExceptionIfMalformedJson() { when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) - .thenReturn(authorizer_malformedJson); + .thenReturn(AUTH_RESPONSE_INVALID_JSON); - PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( - mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, - mockSecretBoxOpenerFactory); + PrivateEncryptedChannelImpl channel = newInstance(); channel.toSubscribeMessage(); } - - /* - TESTING SECRET BOX - */ - - @Test - public void secretBoxOpenerIsCleared() { - PrivateEncryptedChannelImpl channel = new PrivateEncryptedChannelImpl( - mockInternalConnection, getChannelName(), mockAuthorizer, mockFactory, - mockSecretBoxOpenerFactory); - - when(mockAuthorizer.authorize(Matchers.anyString(), Matchers.anyString())) - .thenReturn(authorizer_valid); - when(mockSecretBoxOpenerFactory.create(any())) - .thenReturn(mockSecretBoxOpener); - - channel.toSubscribeMessage(); - - channel.updateState(ChannelState.UNSUBSCRIBED); - verify(mockSecretBoxOpener).clearKey(); - } }