End-to-End Encryption
Important notes:
- In OpenTok.js 2.27.0+, end-to-end encryption will not work when connecting to clients using an earlier version of OpenTok.js. When you upgrade your app to use OpenTok.js 2.27.0+, make sure all clients are using OpenTok.js 2.27.0+ if the app uses end-to-end encryption.
- End-to-end encryption supported on WebKit-based browsers, for example Safari on macOS/iOS and Chrome on iOS, is only available from version OpenTok.js 2.30.0 onwards.
This topic includes details on using end-to-end encryption in the OpenTok.js Vonage Video client SDK:
- Note about Content Security Policies (CSP)
- Initializing a session with a secret
- Checking whether the device supports end-To-end encryption
- Changing the secret
- Events and errors
- Setting a secret in a session without end-to-end encryption
For an overview of end-to-end encryption, see this topic.
Note about Content Security Policies (CSP)
If the script-src directive is set, make sure 'wasm-unsafe-eval' is specified. Otherwise WebAssembly, required for end-to-end encryption, is blocked from loading and executing on the page.
Initializing a session with a secret
End-to-end encrypted sessions are created using server APIs (see Enabling encryption using the REST API). To have a web client join an end-to-end encrypted session, specify an encryption secret when calling the OT.initSession() method:
const session = OT.initSession(
'api-key',
'session-id',
{
encryptionSecret: 'initialEncryptionSecret'
}
});
A valid secret is a string between 8 and 256 characters. The secret can later be changed with the Session.setEncryptionSecret() method (see Changing the secret, below).
Checking whether the browser supports end-to-end encryption
Use the OT.hasEndToEndEncryptionSupport() method to check if the client's browser supports end-to-end encryption:
if (OT.hasEndToEndEncryptionSupport()) {
// Proceed with connecting to the session
}
else {
// Notify the user that they cannot join the session
}
Changing the secret
You can change the secret using the Session.setEncryptionSecret() method after the session has connected:
await session.setEncryptionSecret('newEncryptionSecret');
If the session does not have end-to-end encryption enabled, setting a secret results in an OT_ENCRYPTION_NOT_ENABLED error. See Setting a secret in a session without end-to-end encryption.
Events and errors
Events and errors are essential to managing the behavior of user-driven encryption behavior. End-to-end encryption uses the shared secret model: everyone in the session is expected to use the same secret to encrypt their media and decrypt everyone else's.
A Subscriber object dispatches an encryptionSecretMismatch event when the subscriber is unable to decode a stream's media. It is important to communicate to the user that media is not being received due to an encryption mismatch and not due to a connection failure or audio/video bug:
subscriber.on('encryptionSecretMismatch', () => {
// Activate a UI element communicating that there's been an encryption secret mismatch.
});
Also, it is important to communicate to users that encryption has been successfully enabled. A Subscriber object dispatches an encryptionSecretMatch event when the subscriber is able to decode the stream's media after a previous mismatch.
subscriber.on('encryptionSecretMatch', () => {
// Activate a UI element communicating that the media is being properly decrypted.
});
The Session.connect() callback is invoked with an error if the client tries to connect to an end-to-end encrypted session that was initialized with an invalid encryption secret. A valid secret is a string between 8 and 256 characters. For the best user experience, the application should catch an invalid user supplied secret before calling theOT.initSession() method. In the following example, a session is initialized with an empty (and thus invalid) secret, which causes an error when attempting to connect:
const session = OT.initSession(
'api-key',
'e2ee-session-id',
{
encryptionSecret: '',
}
)
session.connect('token', (error) => {
if (error && error.name === 'OT_INVALID_ENCRYPTION_SECRET') {
/*
The application should communicate that the secret was invalid.
*/
}
});
The Session.connect() callback is invoked with an error if a user attempts to connect to an end-to-end encrypted session in a browser that does not support end-to-end encryption.
const session = OT.initSession(
'api-key',
'e2ee-session-id',
{
encryptionSecret: 'validEncryptionSecret',
}
)
session.connect('token', (error) => {
if (error && error.name === 'OT_UNSUPPORTED_BROWSER') {
/*
The error will have the message 'Tried to connect an e2ee session but the browser does not support e2ee'
The application should communicate that the browser does not support encryption.
*/
}
});
If a user tries to publish in an end-to-end encrypted session without having specified an encryption secret, the Session.publish() callback is invoked with an error. For the best user experience, the application should validate a user-supplied secret before calling the session.publish() method:
session.publish(publisher, (error) => {
if (error && error.name === 'OT_STREAM_CREATE_FAILED') {
/*
The error will have the message 'Tried to publish to an e2ee session but encryption secret was not set'
The application should communicate that the secret was not set.
*/
}
});
If a user tries to subscribe in an end-to-end encrypted session without having specified an encryption secret, the Session.subscribe() callback is invoked with an error. For the best user experience, the application should validate a user-supplied secret before calling the Session.subscribe() method:
subscriber = session.subscribe(stream, target, opts, (error) => {
if (error && error.name === 'OT_UNABLE_TO_SUBSCRIBE') {
/*
The error will have the message 'Tried to subscribe to an e2ee stream but encryption secret was not set'
The application should communicate that the secret was not set.
*/
}
});
Setting a secret in a session without end-to-end encryption
Note: This behavior is available starting with version 2.36.0 of the SDK.
If a session was not created with end-to-end encryption enabled, setting an encryption secret for it results in an OT_ENCRYPTION_NOT_ENABLED error. Where the error surfaces depends on when the secret is set relative to connecting, publishing, and subscribing.
Note: The scenarios in this section assume the client's browser supports end-to-end encryption. In a browser that does not support it, using end-to-end encryption can result in an OT_UNSUPPORTED_BROWSER error instead. See Checking whether the browser supports end-to-end encryption and Events and errors.
Setting the secret before connecting
If the secret is set before connecting — either with the encryptionSecret option of OT.initSession() or by calling Session.setEncryptionSecret() — the session connects successfully. The error is returned in the Session.publish() and Session.subscribe() callbacks:
// Option 1: set the secret when initializing the session
const session = OT.initSession('api-key', 'non-e2ee-session-id', {
encryptionSecret: 'encryptionSecret',
});
// Option 2: set the secret before connecting
// await session.setEncryptionSecret('encryptionSecret');
session.connect('token', (error) => {
if (error) {
return;
}
session.publish(publisher, (error) => {
if (error && error.name === 'OT_ENCRYPTION_NOT_ENABLED') {
/*
The application should communicate that end-to-end encryption
is not enabled for this session.
*/
}
});
session.subscribe(stream, target, opts, (error) => {
if (error && error.name === 'OT_ENCRYPTION_NOT_ENABLED') {
// Same handling as for the publisher.
}
});
});
Setting the secret after connecting
If Session.setEncryptionSecret() is called after the session has connected, the method rejects with the error. Any subsequent call to Session.publish() or Session.subscribe() also returns the error in its callback:
session.connect('token', async (error) => {
if (error) {
return;
}
try {
await session.setEncryptionSecret('encryptionSecret');
} catch (error) {
if (error.name === 'OT_ENCRYPTION_NOT_ENABLED') {
/*
The application should communicate that end-to-end encryption
is not enabled for this session.
*/
}
}
});
Setting the secret while publishing or subscribing
If Session.setEncryptionSecret() is called while the client is already publishing or subscribing, the method rejects with the error, and every active Publisher and Subscriber object dispatches an encryptionNotEnabled event. The SDK then stops those publishers and subscribers automatically, so you do not need to call Session.unpublish() or Session.unsubscribe():
publisher.on('encryptionNotEnabled', (event) => {
// The publisher is stopped by the SDK. No need to call session.unpublish().
});
subscriber.on('encryptionNotEnabled', (event) => {
// The subscriber is stopped by the SDK. No need to call session.unsubscribe().
});
try {
await session.setEncryptionSecret('encryptionSecret');
} catch (error) {
if (error.name === 'OT_ENCRYPTION_NOT_ENABLED') {
// An encryptionNotEnabled event is also dispatched on every
// active publisher and subscriber (handled above).
}
}
Recovering from the error
The session remains valid after any of these errors. To publish or subscribe again, clear the secret by calling Session.setEncryptionSecret() with null. The same Publisher object can then be passed to Session.publish() again:
await session.setEncryptionSecret(null);
session.publish(publisher, (error) => {
// Publishing proceeds without end-to-end encryption.
});