Skip to content

Commit

Permalink
Merge pull request #45 from pusher/end-to-end-encryption
Browse files Browse the repository at this point in the history
End to end encryption
  • Loading branch information
kn100 authored Aug 13, 2018
2 parents 2a4a2a0 + 1ad2088 commit 412688b
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 13 deletions.
3 changes: 1 addition & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
---
language: go
go:
- 1.3
- 1.4
- 1.5
- 1.6
- 1.7
Expand All @@ -12,6 +10,7 @@ install:
- go get github.com/stretchr/testify/assert
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- go get golang.org/x/crypto/nacl/secretbox
- if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
- $HOME/gopath/bin/goveralls -service=travis-ci -repotoken=$COVERALLS_TOKEN
script:
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
1.3.0 / 2018-08-13
==================

* This release adds support for end to end encrypted channels, a new feature for Channels. Read more [in our docs](https://pusher.com/docs/client_api_guide/client_encrypted_channels).

1.2.0 / 2016-05-24
==================

Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This package lets you trigger events to your client and query the state of your

In order to use this library, you need to have a free account on <http://pusher.com>. After registering, you will need the application credentials for your app.

This library requires you to be using at least Go 1.5 or greater.

## Table of Contents

- [Installation](#installation)
Expand Down Expand Up @@ -133,6 +135,28 @@ Setting the `pusher.Client`'s `Cluster` property will make sure requests are sen
```go
client.Cluster = "eu" // in this case requests will be made to api-eu.pusher.com.
```
#### End to End Encryption

This library supports end to end encryption of your private channels. This means that only you and your connected clients will be able to read your messages. Pusher cannot decrypt them. You can enable this feature by following these steps:

1. You should first set up Private channels. This involves [creating an authentication endpoint on your server](https://pusher.com/docs/authenticating_users).

2. Next, Specify your 32 character `EncryptionMasterKey`. This is secret and you should never share this with anyone. Not even Pusher.

```go
client := pusher.Client{
AppId: "your_app_id",
Key: "your_app_key",
Secret: "your_app_secret",
Cluster: "your_app_cluster",
EncryptionMasterKey "abcdefghijklmnopqrstuvwxyzabcdef"
}
```
3. Channels where you wish to use end to end encryption should be prefixed with `private-encrypted-`.

4. Subscribe to these channels in your client, and you're done! You can verify it is working by checking out the debug console on the https://dashboard.pusher.com/ and seeing the scrambled ciphertext.

**Important note: This will not encrypt messages on channels that are not prefixed by private-encrypted-.**

### Google App Engine

Expand Down
31 changes: 26 additions & 5 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package pusher

import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -49,6 +50,7 @@ type Client struct {
Secure bool // true for HTTPS
Cluster string
HttpClient *http.Client
EncryptionMasterKey string //for E2E
}

/*
Expand Down Expand Up @@ -169,15 +171,22 @@ func (c *Client) trigger(channels []string, eventName string, data interface{},
return nil, errors.New("You cannot trigger on more than 10 channels at once")
}

if len(channels) > 1 && encryptedChannelPresent(channels) {
return nil, errors.New("You cannot trigger batch events when using encrypted channels")
}

if !channelsAreValid(channels) {
return nil, errors.New("At least one of your channels' names are invalid")
}
if !validEncryptionKey(c.EncryptionMasterKey) && encryptedChannelPresent(channels) {
return nil, errors.New("Your Encryption key is not of the correct format")
}

if err := validateSocketID(socketID); err != nil {
return nil, err
}

payload, err := createTriggerPayload(channels, eventName, data, socketID)
payload, err := createTriggerPayload(channels, eventName, data, socketID, c.EncryptionMasterKey)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -372,8 +381,8 @@ func (c *Client) AuthenticatePresenceChannel(params []byte, member MemberData) (
}

func (c *Client) authenticateChannel(params []byte, member *MemberData) (response []byte, err error) {
channelName, socketID, err := parseAuthRequestParams(params)

channelName, socketID, err := parseAuthRequestParams(params)
if err != nil {
return
}
Expand All @@ -397,7 +406,15 @@ func (c *Client) authenticateChannel(params []byte, member *MemberData) (respons
stringToSign = strings.Join([]string{stringToSign, jsonUserData}, ":")
}

_response := createAuthMap(c.Key, c.Secret, stringToSign)
var _response map[string]string

if isEncryptedChannel(channelName) {
sharedSecret := generateSharedSecret(channelName, c.EncryptionMasterKey)
sharedSecretB64 := base64.StdEncoding.EncodeToString(sharedSecret[:])
_response = createAuthMap(c.Key, c.Secret, stringToSign, sharedSecretB64)
} else {
_response = createAuthMap(c.Key, c.Secret, stringToSign, "")
}

if member != nil {
_response["channel_data"] = jsonUserData
Expand Down Expand Up @@ -433,10 +450,14 @@ If it is invalid, the first return value will be nil, and an error will be passe
}
*/
func (c *Client) Webhook(header http.Header, body []byte) (*Webhook, error) {

for _, token := range header["X-Pusher-Key"] {
if token == c.Key && checkSignature(header.Get("X-Pusher-Signature"), c.Secret, body) {
return unmarshalledWebhook(body)
unmarshalledWebhooks, err := unmarshalledWebhook(body)
if err != nil {
return unmarshalledWebhooks, err
}
decryptedWebhooks, err := decryptEvents(*unmarshalledWebhooks, c.EncryptionMasterKey)
return decryptedWebhooks, err
}
}
return nil, errors.New("Invalid webhook")
Expand Down
88 changes: 86 additions & 2 deletions crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,23 @@ package pusher
import (
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
// "fmt"
"encoding/json"
"errors"
"io"
"strings"

"golang.org/x/crypto/nacl/secretbox"
)

type EncryptedMessage struct {
Nonce string `json:"nonce"`
Ciphertext string `json:"ciphertext"`
}

func hmacSignature(toSign, secret string) string {
return hex.EncodeToString(hmacBytes([]byte(toSign), []byte(secret)))
}
Expand All @@ -29,9 +40,12 @@ func checkSignature(result, secret string, body []byte) bool {
return hmac.Equal(expected, resultBytes)
}

func createAuthMap(key, secret, stringToSign string) map[string]string {
func createAuthMap(key, secret, stringToSign string, sharedSecret string) map[string]string {
authSignature := hmacSignature(stringToSign, secret)
authString := strings.Join([]string{key, authSignature}, ":")
if sharedSecret != "" {
return map[string]string{"auth": authString, "shared_secret": sharedSecret}
}
return map[string]string{"auth": authString}
}

Expand All @@ -40,3 +54,73 @@ func md5Signature(body []byte) string {
_bodyMD5.Write([]byte(body))
return hex.EncodeToString(_bodyMD5.Sum(nil))
}

func encrypt(channel string, data []byte, encryptionKey string) string {
sharedSecret := generateSharedSecret(channel, encryptionKey)
nonce := generateNonce()
nonceB64 := base64.StdEncoding.EncodeToString(nonce[:])
cipherText := secretbox.Seal([]byte{}, data, &nonce, &sharedSecret)
cipherTextB64 := base64.StdEncoding.EncodeToString(cipherText)
return formatMessage(nonceB64, cipherTextB64)
}

func formatMessage(nonce string, cipherText string) string {
encryptedMessage := &EncryptedMessage{
Nonce: nonce,
Ciphertext: cipherText,
}
json, err := json.Marshal(encryptedMessage)
if err != nil {
panic(err)
}

return string(json)
}

func generateNonce() [24]byte {
var nonce [24]byte
//Trick ReadFull into thinking nonce is a slice
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
panic(err)
}
return nonce
}

func generateSharedSecret(channel string, encryptionKey string) [32]byte {
return sha256.Sum256([]byte(channel + encryptionKey))
}

func decryptEvents(webhookData Webhook, encryptionKey string) (*Webhook, error) {
decryptedWebhooks := &Webhook{}
decryptedWebhooks.TimeMs = webhookData.TimeMs
for _, event := range webhookData.Events {

if isEncryptedChannel(event.Channel) {
var encryptedMessage EncryptedMessage
json.Unmarshal([]byte(event.Data), &encryptedMessage)
cipherTextBytes, decodePayloadErr := base64.StdEncoding.DecodeString(encryptedMessage.Ciphertext)
if decodePayloadErr != nil {
return decryptedWebhooks, decodePayloadErr
}

nonceBytes, decodeNonceErr := base64.StdEncoding.DecodeString(encryptedMessage.Nonce)
if decodeNonceErr != nil {
return decryptedWebhooks, decodeNonceErr
}

// Convert slice to fixed length array for secretbox
var nonce [24]byte
copy(nonce[:], []byte(nonceBytes[:]))

sharedSecret := generateSharedSecret(event.Channel, encryptionKey)
box := []byte(cipherTextBytes)
decryptedBox, ok := secretbox.Open([]byte{}, box, &nonce, &sharedSecret)
if !ok {
return decryptedWebhooks, errors.New("Failed to decrypt event, possibly wrong key?")
}
event.Data = string(decryptedBox)
}
decryptedWebhooks.Events = append(decryptedWebhooks.Events, event)
}
return decryptedWebhooks, nil
}
Loading

0 comments on commit 412688b

Please sign in to comment.