summaryrefslogtreecommitdiff
path: root/vendor/github.com/SherClockHolmes
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/SherClockHolmes')
-rw-r--r--vendor/github.com/SherClockHolmes/webpush-go/.gitignore4
-rw-r--r--vendor/github.com/SherClockHolmes/webpush-go/LICENSE21
-rw-r--r--vendor/github.com/SherClockHolmes/webpush-go/README.md63
-rw-r--r--vendor/github.com/SherClockHolmes/webpush-go/urgency.go26
-rw-r--r--vendor/github.com/SherClockHolmes/webpush-go/vapid.go117
-rw-r--r--vendor/github.com/SherClockHolmes/webpush-go/webpush.go287
6 files changed, 518 insertions, 0 deletions
diff --git a/vendor/github.com/SherClockHolmes/webpush-go/.gitignore b/vendor/github.com/SherClockHolmes/webpush-go/.gitignore
new file mode 100644
index 000000000..13b7c32ac
--- /dev/null
+++ b/vendor/github.com/SherClockHolmes/webpush-go/.gitignore
@@ -0,0 +1,4 @@
+vendor/**
+
+.DS_Store
+*.out
diff --git a/vendor/github.com/SherClockHolmes/webpush-go/LICENSE b/vendor/github.com/SherClockHolmes/webpush-go/LICENSE
new file mode 100644
index 000000000..161eac777
--- /dev/null
+++ b/vendor/github.com/SherClockHolmes/webpush-go/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Ethan Holmes
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/SherClockHolmes/webpush-go/README.md b/vendor/github.com/SherClockHolmes/webpush-go/README.md
new file mode 100644
index 000000000..c313fc6b1
--- /dev/null
+++ b/vendor/github.com/SherClockHolmes/webpush-go/README.md
@@ -0,0 +1,63 @@
+# webpush-go
+
+[![Go Report Card](https://goreportcard.com/badge/github.com/SherClockHolmes/webpush-go)](https://goreportcard.com/report/github.com/SherClockHolmes/webpush-go)
+[![GoDoc](https://godoc.org/github.com/SherClockHolmes/webpush-go?status.svg)](https://godoc.org/github.com/SherClockHolmes/webpush-go)
+
+Web Push API Encryption with VAPID support.
+
+```bash
+go get -u github.com/SherClockHolmes/webpush-go
+```
+
+## Example
+
+For a full example, refer to the code in the [example](example/) directory.
+
+```go
+package main
+
+import (
+ "encoding/json"
+
+ webpush "github.com/SherClockHolmes/webpush-go"
+)
+
+func main() {
+ // Decode subscription
+ s := &webpush.Subscription{}
+ json.Unmarshal([]byte("<YOUR_SUBSCRIPTION>"), s)
+
+ // Send Notification
+ resp, err := webpush.SendNotification([]byte("Test"), s, &webpush.Options{
+ Subscriber: "example@example.com",
+ VAPIDPublicKey: "<YOUR_VAPID_PUBLIC_KEY>",
+ VAPIDPrivateKey: "<YOUR_VAPID_PRIVATE_KEY>",
+ TTL: 30,
+ })
+ if err != nil {
+ // TODO: Handle error
+ }
+ defer resp.Body.Close()
+}
+```
+
+### Generating VAPID Keys
+
+Use the helper method `GenerateVAPIDKeys` to generate the VAPID key pair.
+
+```golang
+privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
+if err != nil {
+ // TODO: Handle error
+}
+```
+
+## Development
+
+1. Install [Go 1.11+](https://golang.org/)
+2. `go mod vendor`
+3. `go test`
+
+#### For other language implementations visit:
+
+[WebPush Libs](https://github.com/web-push-libs)
diff --git a/vendor/github.com/SherClockHolmes/webpush-go/urgency.go b/vendor/github.com/SherClockHolmes/webpush-go/urgency.go
new file mode 100644
index 000000000..97c4a32b4
--- /dev/null
+++ b/vendor/github.com/SherClockHolmes/webpush-go/urgency.go
@@ -0,0 +1,26 @@
+package webpush
+
+// Urgency indicates to the push service how important a message is to the user.
+// This can be used by the push service to help conserve the battery life of a user's device
+// by only waking up for important messages when battery is low.
+type Urgency string
+
+const (
+ // UrgencyVeryLow requires device state: on power and Wi-Fi
+ UrgencyVeryLow Urgency = "very-low"
+ // UrgencyLow requires device state: on either power or Wi-Fi
+ UrgencyLow Urgency = "low"
+ // UrgencyNormal excludes device state: low battery
+ UrgencyNormal Urgency = "normal"
+ // UrgencyHigh admits device state: low battery
+ UrgencyHigh Urgency = "high"
+)
+
+// Checking allowable values for the urgency header
+func isValidUrgency(urgency Urgency) bool {
+ switch urgency {
+ case UrgencyVeryLow, UrgencyLow, UrgencyNormal, UrgencyHigh:
+ return true
+ }
+ return false
+}
diff --git a/vendor/github.com/SherClockHolmes/webpush-go/vapid.go b/vendor/github.com/SherClockHolmes/webpush-go/vapid.go
new file mode 100644
index 000000000..fe2c580a6
--- /dev/null
+++ b/vendor/github.com/SherClockHolmes/webpush-go/vapid.go
@@ -0,0 +1,117 @@
+package webpush
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "math/big"
+ "net/url"
+ "time"
+
+ "github.com/golang-jwt/jwt"
+)
+
+// GenerateVAPIDKeys will create a private and public VAPID key pair
+func GenerateVAPIDKeys() (privateKey, publicKey string, err error) {
+ // Get the private key from the P256 curve
+ curve := elliptic.P256()
+
+ private, x, y, err := elliptic.GenerateKey(curve, rand.Reader)
+ if err != nil {
+ return
+ }
+
+ public := elliptic.Marshal(curve, x, y)
+
+ // Convert to base64
+ publicKey = base64.RawURLEncoding.EncodeToString(public)
+ privateKey = base64.RawURLEncoding.EncodeToString(private)
+
+ return
+}
+
+// Generates the ECDSA public and private keys for the JWT encryption
+func generateVAPIDHeaderKeys(privateKey []byte) *ecdsa.PrivateKey {
+ // Public key
+ curve := elliptic.P256()
+ px, py := curve.ScalarMult(
+ curve.Params().Gx,
+ curve.Params().Gy,
+ privateKey,
+ )
+
+ pubKey := ecdsa.PublicKey{
+ Curve: curve,
+ X: px,
+ Y: py,
+ }
+
+ // Private key
+ d := &big.Int{}
+ d.SetBytes(privateKey)
+
+ return &ecdsa.PrivateKey{
+ PublicKey: pubKey,
+ D: d,
+ }
+}
+
+// getVAPIDAuthorizationHeader
+func getVAPIDAuthorizationHeader(
+ endpoint,
+ subscriber,
+ vapidPublicKey,
+ vapidPrivateKey string,
+ expiration time.Time,
+) (string, error) {
+ // Create the JWT token
+ subURL, err := url.Parse(endpoint)
+ if err != nil {
+ return "", err
+ }
+
+ token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
+ "aud": fmt.Sprintf("%s://%s", subURL.Scheme, subURL.Host),
+ "exp": expiration.Unix(),
+ "sub": fmt.Sprintf("mailto:%s", subscriber),
+ })
+
+ // Decode the VAPID private key
+ decodedVapidPrivateKey, err := decodeVapidKey(vapidPrivateKey)
+ if err != nil {
+ return "", err
+ }
+
+ privKey := generateVAPIDHeaderKeys(decodedVapidPrivateKey)
+
+ // Sign token with private key
+ jwtString, err := token.SignedString(privKey)
+ if err != nil {
+ return "", err
+ }
+
+ // Decode the VAPID public key
+ pubKey, err := decodeVapidKey(vapidPublicKey)
+ if err != nil {
+ return "", err
+ }
+
+ return fmt.Sprintf(
+ "vapid t=%s, k=%s",
+ jwtString,
+ base64.RawURLEncoding.EncodeToString(pubKey),
+ ), nil
+}
+
+// Need to decode the vapid private key in multiple base64 formats
+// Solution from: https://github.com/SherClockHolmes/webpush-go/issues/29
+func decodeVapidKey(key string) ([]byte, error) {
+ bytes, err := base64.URLEncoding.DecodeString(key)
+ if err == nil {
+ return bytes, nil
+ }
+
+ return base64.RawURLEncoding.DecodeString(key)
+}
diff --git a/vendor/github.com/SherClockHolmes/webpush-go/webpush.go b/vendor/github.com/SherClockHolmes/webpush-go/webpush.go
new file mode 100644
index 000000000..4c85ad638
--- /dev/null
+++ b/vendor/github.com/SherClockHolmes/webpush-go/webpush.go
@@ -0,0 +1,287 @@
+package webpush
+
+import (
+ "bytes"
+ "context"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/binary"
+ "errors"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "golang.org/x/crypto/hkdf"
+)
+
+const MaxRecordSize uint32 = 4096
+
+var ErrMaxPadExceeded = errors.New("payload has exceeded the maximum length")
+
+// saltFunc generates a salt of 16 bytes
+var saltFunc = func() ([]byte, error) {
+ salt := make([]byte, 16)
+ _, err := io.ReadFull(rand.Reader, salt)
+ if err != nil {
+ return salt, err
+ }
+
+ return salt, nil
+}
+
+// HTTPClient is an interface for sending the notification HTTP request / testing
+type HTTPClient interface {
+ Do(*http.Request) (*http.Response, error)
+}
+
+// Options are config and extra params needed to send a notification
+type Options struct {
+ HTTPClient HTTPClient // Will replace with *http.Client by default if not included
+ RecordSize uint32 // Limit the record size
+ Subscriber string // Sub in VAPID JWT token
+ Topic string // Set the Topic header to collapse a pending messages (Optional)
+ TTL int // Set the TTL on the endpoint POST request
+ Urgency Urgency // Set the Urgency header to change a message priority (Optional)
+ VAPIDPublicKey string // VAPID public key, passed in VAPID Authorization header
+ VAPIDPrivateKey string // VAPID private key, used to sign VAPID JWT token
+ VapidExpiration time.Time // optional expiration for VAPID JWT token (defaults to now + 12 hours)
+}
+
+// Keys are the base64 encoded values from PushSubscription.getKey()
+type Keys struct {
+ Auth string `json:"auth"`
+ P256dh string `json:"p256dh"`
+}
+
+// Subscription represents a PushSubscription object from the Push API
+type Subscription struct {
+ Endpoint string `json:"endpoint"`
+ Keys Keys `json:"keys"`
+}
+
+// SendNotification calls SendNotificationWithContext with default context for backwards-compatibility
+func SendNotification(message []byte, s *Subscription, options *Options) (*http.Response, error) {
+ return SendNotificationWithContext(context.Background(), message, s, options)
+}
+
+// SendNotificationWithContext sends a push notification to a subscription's endpoint
+// Message Encryption for Web Push, and VAPID protocols.
+// FOR MORE INFORMATION SEE RFC8291: https://datatracker.ietf.org/doc/rfc8291
+func SendNotificationWithContext(ctx context.Context, message []byte, s *Subscription, options *Options) (*http.Response, error) {
+ // Authentication secret (auth_secret)
+ authSecret, err := decodeSubscriptionKey(s.Keys.Auth)
+ if err != nil {
+ return nil, err
+ }
+
+ // dh (Diffie Hellman)
+ dh, err := decodeSubscriptionKey(s.Keys.P256dh)
+ if err != nil {
+ return nil, err
+ }
+
+ // Generate 16 byte salt
+ salt, err := saltFunc()
+ if err != nil {
+ return nil, err
+ }
+
+ // Create the ecdh_secret shared key pair
+ curve := elliptic.P256()
+
+ // Application server key pairs (single use)
+ localPrivateKey, x, y, err := elliptic.GenerateKey(curve, rand.Reader)
+ if err != nil {
+ return nil, err
+ }
+
+ localPublicKey := elliptic.Marshal(curve, x, y)
+
+ // Combine application keys with receiver's EC public key
+ sharedX, sharedY := elliptic.Unmarshal(curve, dh)
+ if sharedX == nil {
+ return nil, errors.New("Unmarshal Error: Public key is not a valid point on the curve")
+ }
+
+ // Derive ECDH shared secret
+ sx, sy := curve.ScalarMult(sharedX, sharedY, localPrivateKey)
+ if !curve.IsOnCurve(sx, sy) {
+ return nil, errors.New("Encryption error: ECDH shared secret isn't on curve")
+ }
+ mlen := curve.Params().BitSize / 8
+ sharedECDHSecret := make([]byte, mlen)
+ sx.FillBytes(sharedECDHSecret)
+
+ hash := sha256.New
+
+ // ikm
+ prkInfoBuf := bytes.NewBuffer([]byte("WebPush: info\x00"))
+ prkInfoBuf.Write(dh)
+ prkInfoBuf.Write(localPublicKey)
+
+ prkHKDF := hkdf.New(hash, sharedECDHSecret, authSecret, prkInfoBuf.Bytes())
+ ikm, err := getHKDFKey(prkHKDF, 32)
+ if err != nil {
+ return nil, err
+ }
+
+ // Derive Content Encryption Key
+ contentEncryptionKeyInfo := []byte("Content-Encoding: aes128gcm\x00")
+ contentHKDF := hkdf.New(hash, ikm, salt, contentEncryptionKeyInfo)
+ contentEncryptionKey, err := getHKDFKey(contentHKDF, 16)
+ if err != nil {
+ return nil, err
+ }
+
+ // Derive the Nonce
+ nonceInfo := []byte("Content-Encoding: nonce\x00")
+ nonceHKDF := hkdf.New(hash, ikm, salt, nonceInfo)
+ nonce, err := getHKDFKey(nonceHKDF, 12)
+ if err != nil {
+ return nil, err
+ }
+
+ // Cipher
+ c, err := aes.NewCipher(contentEncryptionKey)
+ if err != nil {
+ return nil, err
+ }
+
+ gcm, err := cipher.NewGCM(c)
+ if err != nil {
+ return nil, err
+ }
+
+ // Get the record size
+ recordSize := options.RecordSize
+ if recordSize == 0 {
+ recordSize = MaxRecordSize
+ }
+
+ recordLength := int(recordSize) - 16
+
+ // Encryption Content-Coding Header
+ recordBuf := bytes.NewBuffer(salt)
+
+ rs := make([]byte, 4)
+ binary.BigEndian.PutUint32(rs, recordSize)
+
+ recordBuf.Write(rs)
+ recordBuf.Write([]byte{byte(len(localPublicKey))})
+ recordBuf.Write(localPublicKey)
+
+ // Data
+ dataBuf := bytes.NewBuffer(message)
+
+ // Pad content to max record size - 16 - header
+ // Padding ending delimeter
+ dataBuf.Write([]byte("\x02"))
+ if err := pad(dataBuf, recordLength-recordBuf.Len()); err != nil {
+ return nil, err
+ }
+
+ // Compose the ciphertext
+ ciphertext := gcm.Seal([]byte{}, nonce, dataBuf.Bytes(), nil)
+ recordBuf.Write(ciphertext)
+
+ // POST request
+ req, err := http.NewRequest("POST", s.Endpoint, recordBuf)
+ if err != nil {
+ return nil, err
+ }
+
+ if ctx != nil {
+ req = req.WithContext(ctx)
+ }
+
+ req.Header.Set("Content-Encoding", "aes128gcm")
+ req.Header.Set("Content-Length", strconv.Itoa(len(ciphertext)))
+ req.Header.Set("Content-Type", "application/octet-stream")
+ req.Header.Set("TTL", strconv.Itoa(options.TTL))
+
+ // Сheck the optional headers
+ if len(options.Topic) > 0 {
+ req.Header.Set("Topic", options.Topic)
+ }
+
+ if isValidUrgency(options.Urgency) {
+ req.Header.Set("Urgency", string(options.Urgency))
+ }
+
+ expiration := options.VapidExpiration
+ if expiration.IsZero() {
+ expiration = time.Now().Add(time.Hour * 12)
+ }
+
+ // Get VAPID Authorization header
+ vapidAuthHeader, err := getVAPIDAuthorizationHeader(
+ s.Endpoint,
+ options.Subscriber,
+ options.VAPIDPublicKey,
+ options.VAPIDPrivateKey,
+ expiration,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Authorization", vapidAuthHeader)
+
+ // Send the request
+ var client HTTPClient
+ if options.HTTPClient != nil {
+ client = options.HTTPClient
+ } else {
+ client = &http.Client{}
+ }
+
+ return client.Do(req)
+}
+
+// decodeSubscriptionKey decodes a base64 subscription key.
+// if necessary, add "=" padding to the key for URL decode
+func decodeSubscriptionKey(key string) ([]byte, error) {
+ // "=" padding
+ buf := bytes.NewBufferString(key)
+ if rem := len(key) % 4; rem != 0 {
+ buf.WriteString(strings.Repeat("=", 4-rem))
+ }
+
+ bytes, err := base64.StdEncoding.DecodeString(buf.String())
+ if err == nil {
+ return bytes, nil
+ }
+
+ return base64.URLEncoding.DecodeString(buf.String())
+}
+
+// Returns a key of length "length" given an hkdf function
+func getHKDFKey(hkdf io.Reader, length int) ([]byte, error) {
+ key := make([]byte, length)
+ n, err := io.ReadFull(hkdf, key)
+ if n != len(key) || err != nil {
+ return key, err
+ }
+
+ return key, nil
+}
+
+func pad(payload *bytes.Buffer, maxPadLen int) error {
+ payloadLen := payload.Len()
+ if payloadLen > maxPadLen {
+ return ErrMaxPadExceeded
+ }
+
+ padLen := maxPadLen - payloadLen
+
+ padding := make([]byte, padLen)
+ payload.Write(padding)
+
+ return nil
+}