Skip to content

Commit d346cea

Browse files
authored
Merge pull request #247 from pusher/clearSharedSecretOnDisconnected
Add clearing of shared secret on disconnected
2 parents 7afd57e + 0250472 commit d346cea

File tree

4 files changed

+182
-94
lines changed

4 files changed

+182
-94
lines changed

src/main/java/com/pusher/client/channel/impl/PrivateEncryptedChannelImpl.java

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import com.pusher.client.channel.PrivateEncryptedChannel;
77
import com.pusher.client.channel.PrivateEncryptedChannelEventListener;
88
import com.pusher.client.channel.SubscriptionEventListener;
9+
import com.pusher.client.connection.ConnectionEventListener;
10+
import com.pusher.client.connection.ConnectionState;
11+
import com.pusher.client.connection.ConnectionStateChange;
912
import com.pusher.client.connection.impl.InternalConnection;
1013
import com.pusher.client.crypto.nacl.SecretBoxOpener;
1114
import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory;
@@ -22,6 +25,23 @@ public class PrivateEncryptedChannelImpl extends ChannelImpl implements PrivateE
2225
private SecretBoxOpenerFactory secretBoxOpenerFactory;
2326
private SecretBoxOpener secretBoxOpener;
2427

28+
// For not hanging on to shared secret past the Pusher.disconnect() call,
29+
// i.e. when not necessary. Pusher.connect(...) call will trigger re-subscribe
30+
// and hence re-authenticate which creates a new secretBoxOpener.
31+
private ConnectionEventListener disposeSecretBoxOpenerOnDisconnectedListener =
32+
new ConnectionEventListener() {
33+
34+
@Override
35+
public void onConnectionStateChange(ConnectionStateChange change) {
36+
disposeSecretBoxOpener();
37+
}
38+
39+
@Override
40+
public void onError(String message, String code, Exception e) {
41+
// nop
42+
}
43+
};
44+
2545
public PrivateEncryptedChannelImpl(final InternalConnection connection,
2646
final String channelName,
2747
final Authorizer authorizer,
@@ -45,22 +65,38 @@ public void bind(final String eventName, final SubscriptionEventListener listene
4565
super.bind(eventName, listener);
4666
}
4767

48-
private String authenticate() {
68+
@Override
69+
public String toSubscribeMessage() {
70+
String authKey = authenticate();
71+
72+
// create the data part
73+
final Map<Object, Object> dataMap = new LinkedHashMap<>();
74+
dataMap.put("channel", name);
75+
dataMap.put("auth", authKey);
76+
77+
// create the wrapper part
78+
final Map<Object, Object> jsonObject = new LinkedHashMap<>();
79+
jsonObject.put("event", "pusher:subscribe");
80+
jsonObject.put("data", dataMap);
81+
82+
return GSON.toJson(jsonObject);
83+
}
4984

85+
private String authenticate() {
5086
try {
51-
final Map authResponseMap = GSON.fromJson(getAuthResponse(), Map.class);
52-
final String auth = (String) authResponseMap.get("auth");
53-
final String sharedSecret = (String) authResponseMap.get("shared_secret");
87+
@SuppressWarnings("rawtypes") // anything goes in JS
88+
final Map authResponse = GSON.fromJson(getAuthResponse(), Map.class);
89+
90+
final String auth = (String) authResponse.get("auth");
91+
final String sharedSecret = (String) authResponse.get("shared_secret");
5492

5593
if (auth == null || sharedSecret == null) {
5694
throw new AuthorizationFailureException("Didn't receive all the fields expected " +
5795
"from the Authorizer, expected an auth and shared_secret.");
5896
} else {
59-
secretBoxOpener = secretBoxOpenerFactory.create(
60-
Base64.decode(sharedSecret));
97+
createSecretBoxOpener(Base64.decode(sharedSecret));
6198
return auth;
6299
}
63-
64100
} catch (final AuthorizationFailureException e) {
65101
throw e; // pass this upwards
66102
} catch (final Exception e) {
@@ -69,39 +105,38 @@ private String authenticate() {
69105
}
70106
}
71107

72-
@Override
73-
public String toSubscribeMessage() {
74-
75-
String authKey = authenticate();
76-
77-
// create the data part
78-
final Map<Object, Object> dataMap = new LinkedHashMap<Object, Object>();
79-
dataMap.put("channel", name);
80-
dataMap.put("auth", authKey);
81-
82-
// create the wrapper part
83-
final Map<Object, Object> jsonObject = new LinkedHashMap<Object, Object>();
84-
jsonObject.put("event", "pusher:subscribe");
85-
jsonObject.put("data", dataMap);
108+
private void createSecretBoxOpener(byte[] key) {
109+
secretBoxOpener = secretBoxOpenerFactory.create(key);
110+
setListenerToDisposeSecretBoxOpenerOnDisconnected();
111+
}
86112

87-
return GSON.toJson(jsonObject);
113+
private void setListenerToDisposeSecretBoxOpenerOnDisconnected() {
114+
connection.bind(ConnectionState.DISCONNECTED,
115+
disposeSecretBoxOpenerOnDisconnectedListener);
88116
}
89117

90118
@Override
91119
public void updateState(ChannelState state) {
92120
super.updateState(state);
93121

94122
if (state == ChannelState.UNSUBSCRIBED) {
95-
tearDownChannel();
123+
disposeSecretBoxOpener();
96124
}
97125
}
98126

99-
private void tearDownChannel() {
127+
private void disposeSecretBoxOpener() {
100128
if (secretBoxOpener != null) {
101129
secretBoxOpener.clearKey();
130+
secretBoxOpener = null;
131+
removeListenerToDisposeSecretBoxOpenerOnDisconnected();
102132
}
103133
}
104134

135+
private void removeListenerToDisposeSecretBoxOpenerOnDisconnected() {
136+
connection.unbind(ConnectionState.DISCONNECTED,
137+
disposeSecretBoxOpenerOnDisconnectedListener);
138+
}
139+
105140
private String getAuthResponse() {
106141
final String socketId = connection.getSocketId();
107142
return authorizer.authorize(getName(), socketId);

src/main/java/com/pusher/client/example/PrivateEncryptedChannelExampleApp.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,23 @@
1212
public class PrivateEncryptedChannelExampleApp implements
1313
ConnectionEventListener, PrivateEncryptedChannelEventListener {
1414

15-
private String apiKey = "FILL_ME_IN";
15+
private String apiKey = "FILL_ME_IN"; // "key" at https://dashboard.pusher.com
1616
private String channelName = "private-encrypted-channel";
1717
private String eventName = "my-event";
1818
private String cluster = "eu";
1919

20-
private final PrivateEncryptedChannel channel;
20+
private PrivateEncryptedChannel channel;
2121

2222
public static void main(final String[] args) {
2323
new PrivateEncryptedChannelExampleApp(args);
2424
}
2525

2626
private PrivateEncryptedChannelExampleApp(final String[] args) {
27-
28-
if (args.length == 3) {
29-
apiKey = args[0];
30-
channelName = args[1];
31-
eventName = args[2];
32-
cluster = args[3];
27+
switch (args.length) {
28+
case 4: cluster = args[3];
29+
case 3: eventName = args[2];
30+
case 2: channelName = args[1];
31+
case 1: apiKey = args[0];
3332
}
3433

3534
final HttpAuthorizer authorizer = new HttpAuthorizer(
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.pusher.client.channel.impl;
2+
3+
import static org.mockito.Matchers.*;
4+
import static org.mockito.Mockito.*;
5+
6+
import com.pusher.client.Authorizer;
7+
import com.pusher.client.channel.ChannelState;
8+
import com.pusher.client.connection.ConnectionEventListener;
9+
import com.pusher.client.connection.ConnectionState;
10+
import com.pusher.client.connection.ConnectionStateChange;
11+
import com.pusher.client.connection.impl.InternalConnection;
12+
import com.pusher.client.crypto.nacl.SecretBoxOpener;
13+
import com.pusher.client.crypto.nacl.SecretBoxOpenerFactory;
14+
import com.pusher.client.util.Factory;
15+
import org.junit.Before;
16+
import org.junit.Test;
17+
import org.junit.runner.RunWith;
18+
import org.mockito.Mock;
19+
import org.mockito.runners.MockitoJUnitRunner;
20+
import org.mockito.stubbing.Answer;
21+
22+
@RunWith(MockitoJUnitRunner.class)
23+
public class PrivateEncryptedChannelClearsKeyTest {
24+
25+
final String CHANNEL_NAME = "private-encrypted-unit-test-channel";
26+
final String AUTH_RESPONSE = "{\"auth\":\"636a81ba7e7b15725c00:3ee04892514e8a669dc5d30267221f16727596688894712cad305986e6fc0f3c\",\"shared_secret\":\"iBvNoPVYwByqSfg6anjPpEQ2j051b3rt1Vmnb+z5doo=\"}";
27+
28+
@Mock
29+
InternalConnection mockInternalConnection;
30+
@Mock
31+
Authorizer mockAuthorizer;
32+
@Mock
33+
Factory mockFactory;
34+
35+
@Mock
36+
SecretBoxOpenerFactory mockSecretBoxOpenerFactory;
37+
@Mock
38+
SecretBoxOpener mockSecretBoxOpener;
39+
40+
PrivateEncryptedChannelImpl subject;
41+
42+
@Before
43+
public void setUp() {
44+
when(mockAuthorizer.authorize(eq(CHANNEL_NAME), anyString())).thenReturn(AUTH_RESPONSE);
45+
when(mockSecretBoxOpenerFactory.create(any())).thenReturn(mockSecretBoxOpener);
46+
47+
subject = new PrivateEncryptedChannelImpl(mockInternalConnection, CHANNEL_NAME,
48+
mockAuthorizer, mockFactory, mockSecretBoxOpenerFactory);
49+
}
50+
51+
@Test
52+
public void secretBoxOpenerIsClearedOnUnsubscribed() {
53+
subject.toSubscribeMessage();
54+
55+
subject.updateState(ChannelState.UNSUBSCRIBED);
56+
57+
verify(mockSecretBoxOpener).clearKey();
58+
}
59+
60+
@Test
61+
public void secretBoxOpenerIsClearedOnDisconnected() {
62+
doAnswer((Answer<Void>) invocation -> {
63+
ConnectionEventListener l = (ConnectionEventListener) invocation.getArguments()[1];
64+
l.onConnectionStateChange(new ConnectionStateChange(
65+
ConnectionState.DISCONNECTING,
66+
ConnectionState.DISCONNECTED
67+
));
68+
return null;
69+
}).when(mockInternalConnection).bind(eq(ConnectionState.DISCONNECTED), any());
70+
subject.toSubscribeMessage();
71+
72+
verify(mockSecretBoxOpener).clearKey();
73+
}
74+
75+
@Test
76+
public void secretBoxOpenerIsClearedOnceOnUnsubscribedAndThenDisconnected() {
77+
doAnswer((Answer<Void>) invocation -> {
78+
subject.updateState(ChannelState.UNSUBSCRIBED);
79+
80+
ConnectionEventListener l = (ConnectionEventListener) invocation.getArguments()[1];
81+
l.onConnectionStateChange(new ConnectionStateChange(
82+
ConnectionState.DISCONNECTING,
83+
ConnectionState.DISCONNECTED
84+
));
85+
86+
return null;
87+
}).when(mockInternalConnection).bind(eq(ConnectionState.DISCONNECTED), any());
88+
subject.toSubscribeMessage();
89+
90+
verify(mockSecretBoxOpener).clearKey();
91+
}
92+
}

0 commit comments

Comments
 (0)