Skip to content

Commit 412688b

Browse files
authored
Merge pull request #45 from pusher/end-to-end-encryption
End to end encryption
2 parents 2a4a2a0 + 1ad2088 commit 412688b

File tree

8 files changed

+325
-13
lines changed

8 files changed

+325
-13
lines changed

.travis.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
---
22
language: go
33
go:
4-
- 1.3
5-
- 1.4
64
- 1.5
75
- 1.6
86
- 1.7
@@ -12,6 +10,7 @@ install:
1210
- go get github.com/stretchr/testify/assert
1311
- go get github.com/axw/gocov/gocov
1412
- go get github.com/mattn/goveralls
13+
- go get golang.org/x/crypto/nacl/secretbox
1514
- if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
1615
- $HOME/gopath/bin/goveralls -service=travis-ci -repotoken=$COVERALLS_TOKEN
1716
script:

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
1.3.0 / 2018-08-13
2+
==================
3+
4+
* 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).
5+
16
1.2.0 / 2016-05-24
27
==================
38

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ This package lets you trigger events to your client and query the state of your
88

99
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.
1010

11+
This library requires you to be using at least Go 1.5 or greater.
12+
1113
## Table of Contents
1214

1315
- [Installation](#installation)
@@ -133,6 +135,28 @@ Setting the `pusher.Client`'s `Cluster` property will make sure requests are sen
133135
```go
134136
client.Cluster = "eu" // in this case requests will be made to api-eu.pusher.com.
135137
```
138+
#### End to End Encryption
139+
140+
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:
141+
142+
1. You should first set up Private channels. This involves [creating an authentication endpoint on your server](https://pusher.com/docs/authenticating_users).
143+
144+
2. Next, Specify your 32 character `EncryptionMasterKey`. This is secret and you should never share this with anyone. Not even Pusher.
145+
146+
```go
147+
client := pusher.Client{
148+
AppId: "your_app_id",
149+
Key: "your_app_key",
150+
Secret: "your_app_secret",
151+
Cluster: "your_app_cluster",
152+
EncryptionMasterKey "abcdefghijklmnopqrstuvwxyzabcdef"
153+
}
154+
```
155+
3. Channels where you wish to use end to end encryption should be prefixed with `private-encrypted-`.
156+
157+
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.
158+
159+
**Important note: This will not encrypt messages on channels that are not prefixed by private-encrypted-.**
136160

137161
### Google App Engine
138162

client.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package pusher
22

33
import (
4+
"encoding/base64"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -49,6 +50,7 @@ type Client struct {
4950
Secure bool // true for HTTPS
5051
Cluster string
5152
HttpClient *http.Client
53+
EncryptionMasterKey string //for E2E
5254
}
5355

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

174+
if len(channels) > 1 && encryptedChannelPresent(channels) {
175+
return nil, errors.New("You cannot trigger batch events when using encrypted channels")
176+
}
177+
172178
if !channelsAreValid(channels) {
173179
return nil, errors.New("At least one of your channels' names are invalid")
174180
}
181+
if !validEncryptionKey(c.EncryptionMasterKey) && encryptedChannelPresent(channels) {
182+
return nil, errors.New("Your Encryption key is not of the correct format")
183+
}
175184

176185
if err := validateSocketID(socketID); err != nil {
177186
return nil, err
178187
}
179188

180-
payload, err := createTriggerPayload(channels, eventName, data, socketID)
189+
payload, err := createTriggerPayload(channels, eventName, data, socketID, c.EncryptionMasterKey)
181190
if err != nil {
182191
return nil, err
183192
}
@@ -372,8 +381,8 @@ func (c *Client) AuthenticatePresenceChannel(params []byte, member MemberData) (
372381
}
373382

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

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

400-
_response := createAuthMap(c.Key, c.Secret, stringToSign)
409+
var _response map[string]string
410+
411+
if isEncryptedChannel(channelName) {
412+
sharedSecret := generateSharedSecret(channelName, c.EncryptionMasterKey)
413+
sharedSecretB64 := base64.StdEncoding.EncodeToString(sharedSecret[:])
414+
_response = createAuthMap(c.Key, c.Secret, stringToSign, sharedSecretB64)
415+
} else {
416+
_response = createAuthMap(c.Key, c.Secret, stringToSign, "")
417+
}
401418

402419
if member != nil {
403420
_response["channel_data"] = jsonUserData
@@ -433,10 +450,14 @@ If it is invalid, the first return value will be nil, and an error will be passe
433450
}
434451
*/
435452
func (c *Client) Webhook(header http.Header, body []byte) (*Webhook, error) {
436-
437453
for _, token := range header["X-Pusher-Key"] {
438454
if token == c.Key && checkSignature(header.Get("X-Pusher-Signature"), c.Secret, body) {
439-
return unmarshalledWebhook(body)
455+
unmarshalledWebhooks, err := unmarshalledWebhook(body)
456+
if err != nil {
457+
return unmarshalledWebhooks, err
458+
}
459+
decryptedWebhooks, err := decryptEvents(*unmarshalledWebhooks, c.EncryptionMasterKey)
460+
return decryptedWebhooks, err
440461
}
441462
}
442463
return nil, errors.New("Invalid webhook")

crypto.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@ package pusher
33
import (
44
"crypto/hmac"
55
"crypto/md5"
6+
"crypto/rand"
67
"crypto/sha256"
8+
"encoding/base64"
79
"encoding/hex"
8-
// "fmt"
10+
"encoding/json"
11+
"errors"
12+
"io"
913
"strings"
14+
15+
"golang.org/x/crypto/nacl/secretbox"
1016
)
1117

18+
type EncryptedMessage struct {
19+
Nonce string `json:"nonce"`
20+
Ciphertext string `json:"ciphertext"`
21+
}
22+
1223
func hmacSignature(toSign, secret string) string {
1324
return hex.EncodeToString(hmacBytes([]byte(toSign), []byte(secret)))
1425
}
@@ -29,9 +40,12 @@ func checkSignature(result, secret string, body []byte) bool {
2940
return hmac.Equal(expected, resultBytes)
3041
}
3142

32-
func createAuthMap(key, secret, stringToSign string) map[string]string {
43+
func createAuthMap(key, secret, stringToSign string, sharedSecret string) map[string]string {
3344
authSignature := hmacSignature(stringToSign, secret)
3445
authString := strings.Join([]string{key, authSignature}, ":")
46+
if sharedSecret != "" {
47+
return map[string]string{"auth": authString, "shared_secret": sharedSecret}
48+
}
3549
return map[string]string{"auth": authString}
3650
}
3751

@@ -40,3 +54,73 @@ func md5Signature(body []byte) string {
4054
_bodyMD5.Write([]byte(body))
4155
return hex.EncodeToString(_bodyMD5.Sum(nil))
4256
}
57+
58+
func encrypt(channel string, data []byte, encryptionKey string) string {
59+
sharedSecret := generateSharedSecret(channel, encryptionKey)
60+
nonce := generateNonce()
61+
nonceB64 := base64.StdEncoding.EncodeToString(nonce[:])
62+
cipherText := secretbox.Seal([]byte{}, data, &nonce, &sharedSecret)
63+
cipherTextB64 := base64.StdEncoding.EncodeToString(cipherText)
64+
return formatMessage(nonceB64, cipherTextB64)
65+
}
66+
67+
func formatMessage(nonce string, cipherText string) string {
68+
encryptedMessage := &EncryptedMessage{
69+
Nonce: nonce,
70+
Ciphertext: cipherText,
71+
}
72+
json, err := json.Marshal(encryptedMessage)
73+
if err != nil {
74+
panic(err)
75+
}
76+
77+
return string(json)
78+
}
79+
80+
func generateNonce() [24]byte {
81+
var nonce [24]byte
82+
//Trick ReadFull into thinking nonce is a slice
83+
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
84+
panic(err)
85+
}
86+
return nonce
87+
}
88+
89+
func generateSharedSecret(channel string, encryptionKey string) [32]byte {
90+
return sha256.Sum256([]byte(channel + encryptionKey))
91+
}
92+
93+
func decryptEvents(webhookData Webhook, encryptionKey string) (*Webhook, error) {
94+
decryptedWebhooks := &Webhook{}
95+
decryptedWebhooks.TimeMs = webhookData.TimeMs
96+
for _, event := range webhookData.Events {
97+
98+
if isEncryptedChannel(event.Channel) {
99+
var encryptedMessage EncryptedMessage
100+
json.Unmarshal([]byte(event.Data), &encryptedMessage)
101+
cipherTextBytes, decodePayloadErr := base64.StdEncoding.DecodeString(encryptedMessage.Ciphertext)
102+
if decodePayloadErr != nil {
103+
return decryptedWebhooks, decodePayloadErr
104+
}
105+
106+
nonceBytes, decodeNonceErr := base64.StdEncoding.DecodeString(encryptedMessage.Nonce)
107+
if decodeNonceErr != nil {
108+
return decryptedWebhooks, decodeNonceErr
109+
}
110+
111+
// Convert slice to fixed length array for secretbox
112+
var nonce [24]byte
113+
copy(nonce[:], []byte(nonceBytes[:]))
114+
115+
sharedSecret := generateSharedSecret(event.Channel, encryptionKey)
116+
box := []byte(cipherTextBytes)
117+
decryptedBox, ok := secretbox.Open([]byte{}, box, &nonce, &sharedSecret)
118+
if !ok {
119+
return decryptedWebhooks, errors.New("Failed to decrypt event, possibly wrong key?")
120+
}
121+
event.Data = string(decryptedBox)
122+
}
123+
decryptedWebhooks.Events = append(decryptedWebhooks.Events, event)
124+
}
125+
return decryptedWebhooks, nil
126+
}

0 commit comments

Comments
 (0)