import PrivateChannel from './private_channel';
import * as Errors from '../errors';
import Logger from '../logger';
import Pusher from '../pusher';
import { decode as encodeUTF8 } from '@stablelib/utf8';
import { decode as decodeBase64 } from '@stablelib/base64';
import Dispatcher from '../events/dispatcher';
import { PusherEvent } from '../connection/protocol/message-types';
import {
ChannelAuthorizationData,
ChannelAuthorizationCallback
} from '../auth/options';
import * as nacl from 'tweetnacl';
/** Extends private channels to provide encrypted channel interface.
*
* @param {String} name
* @param {Pusher} pusher
*/
export default class EncryptedChannel extends PrivateChannel {
key: Uint8Array = null;
nacl: nacl;
constructor(name: string, pusher: Pusher, nacl: nacl) {
super(name, pusher);
this.nacl = nacl;
}
/** Authorizes the connection to use the channel.
*
* @param {String} socketId
* @param {Function} callback
*/
authorize(socketId: string, callback: ChannelAuthorizationCallback) {
super.authorize(
socketId,
(error: Error | null, authData: ChannelAuthorizationData) => {
if (error) {
callback(error, authData);
return;
}
let sharedSecret = authData['shared_secret'];
if (!sharedSecret) {
callback(
new Error(
`No shared_secret key in auth payload for encrypted channel: ${this.name}`
),
null
);
return;
}
this.key = decodeBase64(sharedSecret);
delete authData['shared_secret'];
callback(null, authData);
}
);
}
trigger(event: string, data: any): boolean {
throw new Errors.UnsupportedFeature(
'Client events are not currently supported for encrypted channels'
);
}
/** Handles an event. For internal use only.
*
* @param {PusherEvent} event
*/
handleEvent(event: PusherEvent) {
var eventName = event.event;
var data = event.data;
if (
eventName.indexOf('pusher_internal:') === 0 ||
eventName.indexOf('pusher:') === 0
) {
super.handleEvent(event);
return;
}
this.handleEncryptedEvent(eventName, data);
}
private handleEncryptedEvent(event: string, data: any): void {
if (!this.key) {
Logger.debug(
'Received encrypted event before key has been retrieved from the authEndpoint'
);
return;
}
if (!data.ciphertext || !data.nonce) {
Logger.error(
'Unexpected format for encrypted event, expected object with `ciphertext` and `nonce` fields, got: ' +
data
);
return;
}
let cipherText = decodeBase64(data.ciphertext);
if (cipherText.length < this.nacl.secretbox.overheadLength) {
Logger.error(
`Expected encrypted event ciphertext length to be ${this.nacl.secretbox.overheadLength}, got: ${cipherText.length}`
);
return;
}
let nonce = decodeBase64(data.nonce);
if (nonce.length < this.nacl.secretbox.nonceLength) {
Logger.error(
`Expected encrypted event nonce length to be ${this.nacl.secretbox.nonceLength}, got: ${nonce.length}`
);
return;
}
let bytes = this.nacl.secretbox.open(cipherText, nonce, this.key);
if (bytes === null) {
Logger.debug(
'Failed to decrypt an event, probably because it was encrypted with a different key. Fetching a new key from the authEndpoint...'
);
// Try a single time to retrieve a new auth key and decrypt the event with it
// If this fails, a new key will be requested when a new message is received
this.authorize(this.pusher.connection.socket_id, (error, authData) => {
if (error) {
Logger.error(
`Failed to make a request to the authEndpoint: ${authData}. Unable to fetch new key, so dropping encrypted event`
);
return;
}
bytes = this.nacl.secretbox.open(cipherText, nonce, this.key);
if (bytes === null) {
Logger.error(
`Failed to decrypt event with new key. Dropping encrypted event`
);
return;
}
this.emit(event, this.getDataToEmit(bytes));
return;
});
return;
}
this.emit(event, this.getDataToEmit(bytes));
}
// Try and parse the decrypted bytes as JSON. If we can't parse it, just
// return the utf-8 string
getDataToEmit(bytes: Uint8Array): string {
let raw = encodeUTF8(bytes);
try {
return JSON.parse(raw);
} catch {
return raw;
}
}
}
|