Messages you send via Pusher Channels have always been encrypted in transit to and from Pusher. But until recently, there wasn’t end-to-end encryption. With our new end-to-end encryption feature, you can add end-to-end encryption to your application with one line in your server configuration!
This means you can prevent Pusher from reading all sensitive content (such as medical or financial data). In this post, we’ll show the problem this feature solves, how this feature solves that problem, and how you can use this feature, with a practical example.
What problem does end-to-end encryption solve?
Consider a “doctor’s office” app which lets patients submit appointment requests to their doctors. When a new appointment request comes in, you want to inform the doctor’s office as quickly as possible, so that they can arrange the appointment. For this realtime delivery, you can use Pusher Channels.
These appointment requests have sensitive details. For example, a patient might categorize his problem as “anxiety”. In such cases, the “principle of least privilege” applies, which means you need to restrict access to only the people that require it. This list includes the patient, the doctor, and the doctor’s secretary.
As developers, we have several tools to enforce the principle of least privilege: your login system, TLS, and “private channels”. Your “doctor’s office” login system prevents attackers from impersonating a privileged user such as a doctor.
TLS (SSL) prevents attackers from reading or manipulating data sent between remote parties on the open internet. Pusher Channels’ “private channels” feature prevents attackers from reading sensitive data that was intended for privileged users.
End-to-end encryption is another important tool to help you enforce the principle of least privilege. It removes Pusher from the list of privileged entities that can read and write sensitive data such as appointment requests.
Pusher Channels holds long-lived open connections to your users’ apps so that you’re always in instant contact with your users. To do this, Pusher Channels sits as a proxy between your server and your users.
As a proxy, Pusher Channels is technically a man-in-the-middle, a privileged user. Despite TLS being used from your server to Pusher Channels, and from Pusher Channels to your users, messages are in plaintext while they flow through our internal network.
End-to-end encryption solves this problem, by ensuring that sensitive data is encrypted even while it flows through our internal network.
How do I use end-to-end encryption?
Let’s implement end-to-end encryption in that “doctor’s office” app. In this example, we have a web app for your users (doctors), and a server written in PHP. (This example is published in this GitHub repository.)
Our end-to-end encryption feature is implemented on a new channel type, which begins with
private-encrypted-. For example, the doctor with ID 42 in your system might have the channel
private-encrypted-dr-42. When your server learns of a new appointment request, it will trigger an event on that encrypted channel:
$pusher->trigger( 'private-encrypted-dr-' . $_POST['doctor-id'], // an encrypted channel 'appointment-request', array("patient-id" => $_POST['patient-id'], "category" => $_POST['category']) );
Notice that the body of this event contains sensitive health information: the “category” of the appointment, such as “anxiety”. But by switching to
private-encrypted-, Pusher is unable to see this information, because the
$pusher->trigger function will encrypt the body. To do so, the
$pusher object needs a master key. You provide this master key when initializing the library:
$pusher = new Pusher\Pusher( getenv('CHANNELS_APP_KEY'), getenv('CHANNELS_APP_SECRET'), getenv('CHANNELS_APP_ID'), array( 'cluster' => getenv('CHANNELS_APP_CLUSTER'), 'useTLS' => true, 'encryption_master_key' => '2tPOaZOZJlQZQhdj4D8kgIhjGfYGcj9i' ) );
Above, we use the master key
2tPOaZOZJlQZQhdj4D8kgIhjGfYGcj9i. You must generate this master key yourself. You can use the
openssl tool for this:
$ openssl rand -base64 24 2tPOaZOZJlQZQhdj4D8kgIhjGfYGcj9i
If you view the debug console for your dashboard when you trigger an event, you should see an encrypted payload:
Now for the other side: decrypting. Happily, there is nothing to do here: pusher-js automatically decrypts the event body on
How does end-to-end encryption work?
For each encrypted channel, there is a shared secret between your server and the subscribers to that channel. This channel shared secret is used by the server to encrypt a message to that channel, and by each subscriber to decrypt the message when received.
The channel shared secret is provided to subscribers in the response to an auth endpoint request. This requires no changes to your auth endpoint code:
// This will include the channel shared secret automatically echo $pusher->socket_auth('private-encrypted-dr-42', $socket_id);
The channel shared secret is derived by the SDK from the channel name and your master key. This means that your server does not have to keep track of a large number of shared secrets. For more details, this series of slides shows the rationale behind the design:
What does end-to-end encryption not do?
Our implementation of end-to-end encryption provides the key property that Pusher cannot read or forge your messages. However, there are some properties that this does not provide:
- Forward secrecy. A leaked secret can be used to decrypt past messages. Forward secrecy would be difficult to implement because known methods require ordered and guaranteed message delivery.
- Prevention against replay attacks. In theory, Pusher could deliver your message multiple times, with malicious intent. (Of course, we don’t do this!)
- Encrypted client events. Client events are still readable by Pusher. (We are considering encrypted client events in future.)
- Encrypted channel names and encrypted event names. The channel name and event name are still visible to Pusher. (We could in future encrypt both of these.)
Where to go from here?
End-to-end encryption is in beta! It’s currently implemented in most SDKs. See our documentation to try it out! If you would like to see support in more SDKs, or for any other suggestions or advice, please contact us at firstname.lastname@example.org.