forked from gauntface/web-push-go
/
encrypt.go
345 lines (301 loc) · 10.9 KB
/
encrypt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package webpush provides helper functions for sending encrpyted payloads
// using the Web Push protocol.
//
// Sending a message:
// import (
// "strings"
// "github.com/googlechrome/push-encryption-go/webpush"
// )
//
// func main() {
// // The values that make up the Subscription struct come from the browser
// sub := &webpush.Subscription{endpoint, key, auth}
// webpush.Send(nil, sub, "Yay! Web Push!", nil)
// }
//
// You can turn a JSON string representation of a PushSubscription object you
// collected from the browser into a Subscription struct with a helper function.
//
// var exampleJSON = []byte(`{"endpoint": "...", "keys": {"p256dh": "...", "auth": "..."}}`)
// sub, err := SubscriptionFromJSON(exampleJSON)
//
// If the push service requires an authentication header (notably Google Cloud
// Messaging, used by Chrome) then you can add that as a fourth parameter:
//
// if strings.Contains(sub.Endpoint, "https://android.googleapis.com/gcm/send/") {
// webpush.Send(nil, sub, "A message for Chrome", myGCMKey)
// }
package webpush
import (
"crypto/aes"
"crypto/cipher"
"crypto/elliptic"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"strings"
)
const (
maxPayloadLength = 4078
)
var (
authInfo = []byte("Content-Encoding: auth\x00")
curve = elliptic.P256()
// Generate a random EC256 key pair. Overridable for testing.
// Returns priv as a 16-byte point, and pub in uncompressed format, 33 bytes.
randomKey = func() (priv []byte, pub []byte, err error) {
priv, x, y, err := elliptic.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, err
}
return priv, elliptic.Marshal(curve, x, y), nil
}
// Generate a random salt for the encryption. Overridable for testing.
randomSalt = func() ([]byte, error) {
salt := make([]byte, 16)
_, err := rand.Read(salt)
if err != nil {
return nil, err
}
return salt, nil
}
)
// Subscription holds the useful values from a PushSubscription object acquired
// from the browser
type Subscription struct {
// Endpoint is the URL to send the Web Push message to. Comes from the
// endpoint field of the PushSubscription.
Endpoint string
// Key is the client's public key. From the keys.p256dh field.
Key []byte
// Auth is a value used by the client to validate the encryption. From the
// keys.auth field.
Auth []byte
}
// SubscriptionFromJSON is a convenience function that takes a JSON encoded
// PushSubscription object acquired from the browser and returns a pointer to a
// Subscription
func SubscriptionFromJSON(b []byte) (*Subscription, error) {
var sub struct {
Endpoint string
Keys struct {
P256dh string
Auth string
}
}
if err := json.Unmarshal(b, &sub); err != nil {
return nil, err
}
b64 := base64.URLEncoding.WithPadding(base64.NoPadding)
// Chrome < 52 incorrectly adds padding when Base64 encoding the values, so
// we need to strip that out
key, err := b64.DecodeString(strings.TrimRight(sub.Keys.P256dh, "="))
if err != nil {
return nil, err
}
auth, err := b64.DecodeString(strings.TrimRight(sub.Keys.Auth, "="))
if err != nil {
return nil, err
}
return &Subscription{sub.Endpoint, key, auth}, nil
}
// TODO: rename ServerPublicKey to 'dhKey' or 'tmpKey' - to avoid confusion
// with the VAPID ServerPublicKey
// EncryptionResult stores the result of encrypting a message. The ciphertext is
// the actual encrypted message, while the salt and server public key are
// required to be sent to the client so that the message can be decrypted.
type EncryptionResult struct {
Ciphertext []byte
Salt []byte
ServerPublicKey []byte
}
// TODO: input should be a []byte ( proto, etc )
// Encrypt a message such that it can be sent using the Web Push protocol.
// You can find out more about the various pieces:
// - https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding
// - https://en.wikipedia.org/wiki/Elliptic_curve_Diffie%E2%80%93Hellman
// - https://tools.ietf.org/html/draft-ietf-webpush-encryption
func Encrypt(sub *Subscription, message string) (*EncryptionResult, error) {
plaintext := []byte(message)
// Use ECDH to derive a shared secret between us and the client. We generate
// a fresh private/public key pair at random every time we encrypt.
serverPrivateKey, serverPublicKey, err := randomKey()
if err != nil {
return nil, err
}
return EncryptWithTempKey(sub, plaintext, serverPrivateKey, serverPublicKey)
}
// Encrypt a message using Web Push protocol, reusing the temp key.
// A new salt will be used. This is ~20% faster.
func EncryptWithTempKey(sub *Subscription, plaintext []byte,
serverPrivateKey, serverPublicKey []byte) (*EncryptionResult, error) {
if len(plaintext) > maxPayloadLength {
return nil, fmt.Errorf("payload is too large. The max number of bytes is %d, input is %d bytes ", maxPayloadLength, len(plaintext))
}
if len(sub.Key) == 0 {
return nil, fmt.Errorf("subscription must include the client's public key")
}
if len(sub.Auth) == 0 {
return nil, fmt.Errorf("subscription must include the client's auth value")
}
salt, err := randomSalt()
if err != nil {
return nil, err
}
// Use ECDH to derive a shared secret between us and the client. We generate
// a fresh private/public key pair at random every time we encrypt.
secret, err := sharedSecret(curve, sub.Key, serverPrivateKey)
if err != nil {
return nil, err
}
// Derive a Pseudo-Random Key (prk) that can be used to further derive our
// other encryption parameters. These derivations are described in
// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00
prk := hkdf(sub.Auth, secret, authInfo, 32)
// Derive the Content Encryption Key and nonce
ctx := newContext(sub.Key, serverPublicKey)
cek := newCEK(ctx, salt, prk)
nonce := newNonce(ctx, salt, prk)
// Do the actual encryption
ciphertext, err := encrypt(plaintext, cek, nonce)
if err != nil {
return nil, err
}
// Return all of the values needed to construct a Web Push HTTP request.
return &EncryptionResult{ciphertext, salt, serverPublicKey}, nil
}
func newCEK(ctx, salt, prk []byte) []byte {
info := newInfo("aesgcm", ctx)
return hkdf(salt, prk, info, 16)
}
func newNonce(ctx, salt, prk []byte) []byte {
info := newInfo("nonce", ctx)
return hkdf(salt, prk, info, 12)
}
// Creates a context for deriving encyption parameters, as described in
// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00.
// The 'context' in this case is just the public keys of both client and server.
// The keys should always be 65 bytes each. The format of the keys is
// described in section 4.3.6 of the (sadly not freely linkable) ANSI X9.62
// specification.
func newContext(clientPublicKey, serverPublicKey []byte) []byte {
// The context format is:
// 0x00 || length(clientPublicKey) || clientPublicKey ||
// length(serverPublicKey) || serverPublicKey
// The lengths are 16-bit, Big Endian, unsigned integers so take 2 bytes each.
cplen := uint16(len(clientPublicKey))
cplenbuf := make([]byte, 2)
binary.BigEndian.PutUint16(cplenbuf, cplen)
splen := uint16(len(serverPublicKey))
splenbuf := make([]byte, 2)
binary.BigEndian.PutUint16(splenbuf, splen)
var ctx []byte
ctx = append(ctx, 0)
ctx = append(ctx, cplenbuf...)
ctx = append(ctx, []byte(clientPublicKey)...)
ctx = append(ctx, splenbuf...)
ctx = append(ctx, []byte(serverPublicKey)...)
return ctx
}
// Returns an info record. See sections 3.2 and 3.3 of
// https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00.
// The context argument should match what newContext creates
func newInfo(infoType string, context []byte) []byte {
var info []byte
info = append(info, []byte("Content-Encoding: ")...)
info = append(info, []byte(infoType)...)
info = append(info, 0)
info = append(info, []byte("P-256")...)
info = append(info, context...)
return info
}
// HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
//
// This is used to derive a secure encryption key from a mostly-secure shared
// secret.
//
// This is a partial implementation of HKDF tailored to our specific purposes.
// In particular, for us the value of N will always be 1, and thus T always
// equals HMAC-Hash(PRK, info | 0x01). This is true because the maximum output
// length we need/allow is 32.
//
// See https://www.rfc-editor.org/rfc/rfc5869.txt
func hkdf(salt, ikm, info []byte, length int) []byte {
// HMAC length for SHA256 is 32 bytes, so that is the maximum result length.
if length > 32 {
panic("Can only produce HKDF outputs up to 32 bytes long")
}
// Extract
mac := hmac.New(sha256.New, salt)
mac.Write(ikm)
prk := mac.Sum(nil)
// Expand
mac = hmac.New(sha256.New, prk)
mac.Write(info)
mac.Write([]byte{1})
return mac.Sum(nil)[0:length]
}
// Encrypt the plaintext message using AES128/GCM
func encrypt(plaintext, key, nonce []byte) ([]byte, error) {
// Add padding. There is a uint16 size followed by that number of bytes of
// padding.
// TODO: Right now we leave the size at zero. We should add a padding option
// that allows the payload size to be obscured.
padding := make([]byte, 2)
data := append(padding, plaintext...)
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
// TODO: to reduce allocations, allow out buffer to be passed in
// (all temp buffers can be kept in a context, size is bound)
return gcm.Seal([]byte{}, nonce, data, nil), nil
}
// Decrypt the message using AES128/GCM
func decrypt(ciphertext, key, nonce []byte) (plaintext []byte, err error) {
c, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(c)
if err != nil {
return nil, err
}
plaintext, err = gcm.Open([]byte{}, nonce, ciphertext, nil)
if err == nil && len(plaintext) >= 2 {
// TODO: read the first 2 bytes, skip that many bytes padding
plaintext = plaintext[2:]
}
return
}
// Given the coordinates of a party A's public key and the bytes of party B's
// private key, compute a shared secret.
func sharedSecret(curve elliptic.Curve, pub, priv []byte) ([]byte, error) {
publicX, publicY := elliptic.Unmarshal(curve, pub)
if publicX == nil {
return nil, fmt.Errorf("Couldn't unmarshal public key. Not a valid point on the curve.")
}
x, _ := curve.ScalarMult(publicX, publicY, priv)
return x.Bytes(), nil
}