Skip to content

Commit

Permalink
Add end-to-end encryption support (#237)
Browse files Browse the repository at this point in the history
* Added e2ee support

* Added validation for SessionProperties

* Only include e2ee if true

* Update SessionProperties javadoc

Co-authored-by: Jeff Swartz <[email protected]>

* Minor docs edit

---------

Co-authored-by: Jeff Swartz <[email protected]>
  • Loading branch information
SMadani and jeffswartz authored May 4, 2023
1 parent a615f1b commit a28e38c
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 44 deletions.
2 changes: 1 addition & 1 deletion src/main/java/com/opentok/OpenTok.java
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ public String generateToken(String sessionId) throws OpenTokException {
*/
public Session createSession(SessionProperties properties) throws OpenTokException {
final SessionProperties _properties = properties != null ? properties : new SessionProperties.Builder().build();
final Map<String, Collection<String>> params = _properties.toMap();
final Map<String, List<String>> params = _properties.toMap();
final String response = client.createSession(params);

try {
Expand Down
7 changes: 1 addition & 6 deletions src/main/java/com/opentok/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,13 @@
* to get the session ID.
*/
public class Session {

private String sessionId;
private int apiKey;
private String apiSecret;
private SessionProperties properties;

protected Session(String sessionId, int apiKey, String apiSecret) {
this.sessionId = sessionId;
this.apiKey = apiKey;
this.apiSecret = apiSecret;
this.properties = new SessionProperties.Builder().build();
this(sessionId, apiKey, apiSecret, new SessionProperties.Builder().build());
}

protected Session(String sessionId, int apiKey, String apiSecret, SessionProperties properties) {
Expand Down Expand Up @@ -83,7 +79,6 @@ public SessionProperties getProperties() {
* @see #generateToken(TokenOptions tokenOptions)
*/
public String generateToken() throws OpenTokException {
// NOTE: maybe there should be a static object for the defaultTokenOptions?
return this.generateToken(new TokenOptions.Builder().build());
}

Expand Down
75 changes: 57 additions & 18 deletions src/main/java/com/opentok/SessionProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@
import com.opentok.exception.InvalidArgumentException;
import org.apache.commons.validator.routines.InetAddressValidator;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.*;


/**
Expand All @@ -23,16 +20,16 @@
* @see OpenTok#createSession(com.opentok.SessionProperties properties)
*/
public class SessionProperties {


private String location;
private MediaMode mediaMode;
private ArchiveMode archiveMode;
private boolean e2ee;

private SessionProperties(Builder builder) {
this.location = builder.location;
this.mediaMode = builder.mediaMode;
this.archiveMode = builder.archiveMode;
this.e2ee = builder.e2ee;
}

/**
Expand All @@ -41,10 +38,10 @@ private SessionProperties(Builder builder) {
* @see SessionProperties
*/
public static class Builder {
private String location = null;
private String location;
private MediaMode mediaMode = MediaMode.RELAYED;
private ArchiveMode archiveMode = ArchiveMode.MANUAL;

private boolean e2ee = false;

/**
* Call this method to set an IP address that the OpenTok servers will use to
Expand Down Expand Up @@ -108,7 +105,7 @@ public Builder mediaMode(MediaMode mediaMode) {
/**
* Call this method to determine whether the session will be automatically archived (<code>ArchiveMode.ALWAYS</code>)
* or not (<code>ArchiveMode.MANUAL</code>).
*
* <p>
* Using an always archived session also requires the routed media mode (<code>MediaMode.ROUTED</code>).
*
* @param archiveMode The Archive mode.
Expand All @@ -120,19 +117,43 @@ public Builder archiveMode(ArchiveMode archiveMode) {
return this;
}

/**
* Enables <a href="https://tokbox.com/developer/guides/end-to-end-encryption">end-to-end encryption</a> for a routed session.
* You must also set {@link #mediaMode(MediaMode)} to {@linkplain MediaMode#ROUTED} when
* calling this method.
*
* @return The SessionProperties.Builder object with the e2ee property set to {@code true}.
*/
public Builder endToEndEncryption() {
this.e2ee = true;
return this;
}

/**
* Builds the SessionProperties object.
*
* @return The SessionProperties object.
*/
public SessionProperties build() {
// Would throw in this case, but would introduce a backwards incompatible change.
//if (this.archiveMode == ArchiveMode.ALWAYS && this.mediaMode != MediaMode.ROUTED) {
// throw new InvalidArgumentException("A session with always archive mode must also have the routed media mode.");
//}
if (this.archiveMode == ArchiveMode.ALWAYS && this.mediaMode != MediaMode.ROUTED) {
throw new IllegalStateException(
"A session with ALWAYS archive mode must also have the ROUTED media mode."
);
}
if (e2ee && mediaMode != MediaMode.ROUTED) {
throw new IllegalStateException(
"A session with RELAYED media mode cannot have end-to-end encryption enabled."
);
}
if (e2ee && archiveMode == ArchiveMode.ALWAYS) {
throw new IllegalStateException(
"A session with ALWAYS archive mode cannot have end-to-end encryption enabled."
);
}
return new SessionProperties(this);
}
}

/**
* The location hint IP address. See {@link SessionProperties.Builder#location(String location)}.
*/
Expand All @@ -158,25 +179,43 @@ public ArchiveMode archiveMode() {
return archiveMode;
}

/**
* Defines whether the session will use
* <a href="https://tokbox.com/developer/guides/end-to-end-encryption">end-to-end encryption</a>.
* See {@link com.opentok.SessionProperties.Builder#endToEndEncryption()}.
*
*
* @return {@code true} if end-to-end encryption is enabled, {@code false} otherwise.
*/
public boolean isEndToEndEncrypted() {
return e2ee;
}

/**
* Returns the session properties as a Map.
*/
public Map<String, Collection<String>> toMap() {
Map<String, Collection<String>> params = new HashMap<>();
public Map<String, List<String>> toMap() {
Map<String, List<String>> params = new HashMap<>();
if (null != location) {
ArrayList<String> valueList = new ArrayList<>();
ArrayList<String> valueList = new ArrayList<>(1);
valueList.add(location);
params.put("location", valueList);
}

ArrayList<String> mediaModeValueList = new ArrayList<>();
ArrayList<String> mediaModeValueList = new ArrayList<>(1);
mediaModeValueList.add(mediaMode.toString());
params.put("p2p.preference", mediaModeValueList);

ArrayList<String> archiveModeValueList = new ArrayList<>();
ArrayList<String> archiveModeValueList = new ArrayList<>(1);
archiveModeValueList.add(archiveMode.toString());
params.put("archiveMode", archiveModeValueList);

if (e2ee) {
ArrayList<String> e2eeValueList = new ArrayList<>(1);
e2eeValueList.add("" + e2ee);
params.put("e2ee", e2eeValueList);
}

return params;
}

Expand Down
12 changes: 2 additions & 10 deletions src/main/java/com/opentok/util/HttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,9 @@ private HttpClient(Builder builder) {
this.apiUrl = builder.apiUrl;
}

public String createSession(Map<String, Collection<String>> params) throws RequestException {
Map<String, List<String>> paramsWithList = null;
if (params != null) {
paramsWithList = new HashMap<>();
for (Entry<String, Collection<String>> entry : params.entrySet()) {
paramsWithList.put(entry.getKey(), new ArrayList<>(entry.getValue()));
}
}

public String createSession(Map<String, List<String>> params) throws RequestException {
Future<Response> request = this.preparePost(this.apiUrl + "/session/create")
.setFormParams(paramsWithList)
.setFormParams(params)
.setHeader("Accept", "application/json") // XML version is deprecated
.execute();

Expand Down
80 changes: 71 additions & 9 deletions src/test/java/com/opentok/test/OpenTokTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.opentok.exception.OpenTokException;
import com.opentok.exception.RequestException;
import org.apache.commons.lang.StringUtils;
import org.checkerframework.common.returnsreceiver.qual.This;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
Expand Down Expand Up @@ -240,6 +241,7 @@ public void testCreateDefaultSession() throws OpenTokException {
Session session = sdk.createSession();

assertNotNull(session);
assertFalse(session.getProperties().isEndToEndEncrypted());
assertEquals(apiKey, session.getApiKey());
assertEquals(sessionId, session.getSessionId());
assertEquals(MediaMode.RELAYED, session.getProperties().mediaMode());
Expand Down Expand Up @@ -272,6 +274,8 @@ public void testCreateRoutedSession() throws OpenTokException {
Session session = sdk.createSession(properties);

assertNotNull(session);
assertEquals(properties, session.getProperties());
assertFalse(session.getProperties().isEndToEndEncrypted());
assertEquals(apiKey, session.getApiKey());
assertEquals(sessionId, session.getSessionId());
assertEquals(MediaMode.ROUTED, session.getProperties().mediaMode());
Expand Down Expand Up @@ -304,6 +308,8 @@ public void testCreateLocationHintSession() throws OpenTokException {
Session session = sdk.createSession(properties);

assertNotNull(session);
assertEquals(properties, session.getProperties());
assertFalse(session.getProperties().isEndToEndEncrypted());
assertEquals(apiKey, session.getApiKey());
assertEquals(sessionId, session.getSessionId());
assertEquals(MediaMode.RELAYED, session.getProperties().mediaMode());
Expand All @@ -317,6 +323,41 @@ public void testCreateLocationHintSession() throws OpenTokException {
Helpers.verifyUserAgent();
}

@Test
public void testCreateEncryptedSession() throws OpenTokException {
String sessionId = "SESSION1D";
stubFor(post(urlEqualTo(SESSION_CREATE))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("[{\"session_id\":\"" + sessionId + "\",\"project_id\":\"00000000\"," +
"\"partner_id\":\"123456\"," +
"\"create_dt\":\"Mon Mar 17 00:41:31 PDT 2014\"," +
"\"media_server_url\":\"\"}]")));

SessionProperties properties = new SessionProperties.Builder()
.endToEndEncryption()
.mediaMode(MediaMode.ROUTED)
.build();
Session session = sdk.createSession(properties);

assertNotNull(session);
assertEquals(properties, session.getProperties());
assertTrue(session.getProperties().isEndToEndEncrypted());
assertEquals(apiKey, session.getApiKey());
assertEquals(sessionId, session.getSessionId());
assertEquals(MediaMode.ROUTED, session.getProperties().mediaMode());
assertEquals(ArchiveMode.MANUAL, session.getProperties().archiveMode());
assertNull(session.getProperties().getLocation());

verify(postRequestedFor(urlMatching(SESSION_CREATE))
// NOTE: this is a pretty bad way to verify, ideally we can decode the body and then query the object
.withRequestBody(matching(".*e2ee=true.*")));
assertTrue(Helpers.verifyTokenAuth(apiKey, apiSecret,
findAll(postRequestedFor(urlMatching(SESSION_CREATE)))));
Helpers.verifyUserAgent();
}

@Test
public void testCreateAlwaysArchivedSession() throws OpenTokException {
String sessionId = "SESSIONID";
Expand All @@ -331,6 +372,7 @@ public void testCreateAlwaysArchivedSession() throws OpenTokException {

SessionProperties properties = new SessionProperties.Builder()
.archiveMode(ArchiveMode.ALWAYS)
.mediaMode(MediaMode.ROUTED)
.build();
Session session = sdk.createSession(properties);

Expand All @@ -355,16 +397,36 @@ public void testCreateBadSession() throws OpenTokException {
.build();
}

// This is not part of the API because it would introduce a backwards incompatible change.
// @Test(expected = InvalidArgumentException.class)
// public void testCreateInvalidAlwaysArchivedAndRelayedSession() throws OpenTokException {
// SessionProperties properties = new SessionProperties.Builder()
// .mediaMode(MediaMode.RELAYED)
// .archiveMode(ArchiveMode.ALWAYS)
// .build();
// }
@Test(expected = IllegalStateException.class)
public void testCreateInvalidAlwaysArchivedAndRelayedSession() {
new SessionProperties.Builder()
.mediaMode(MediaMode.RELAYED)
.archiveMode(ArchiveMode.ALWAYS)
.build();
}

@Test(expected = IllegalStateException.class)
public void testCreateInvalidAlwaysArchivedAndE2eeSession() {
new SessionProperties.Builder()
.mediaMode(MediaMode.ROUTED)
.endToEndEncryption()
.archiveMode(ArchiveMode.ALWAYS)
.build();
}

// TODO: test session creation conditions that result in errors
@Test(expected = IllegalStateException.class)
public void testCreateInvalidRelayedMediaAndE2eeSession() {
new SessionProperties.Builder()
.archiveMode(ArchiveMode.MANUAL)
.endToEndEncryption()
.mediaMode(MediaMode.RELAYED)
.build();
}

@Test(expected = IllegalStateException.class)
public void testCreateInvalidE2eeSessionDefault() {
new SessionProperties.Builder().endToEndEncryption().build();
}

@Test
public void testTokenDefault() throws
Expand Down

0 comments on commit a28e38c

Please sign in to comment.