diff --git a/go.mod b/go.mod index 19fecf914..6ad90f6cc 100644 --- a/go.mod +++ b/go.mod @@ -65,6 +65,7 @@ require ( github.com/ncruces/go-sqlite3 v0.20.3 github.com/oklog/ulid v1.3.1 github.com/prometheus/client_golang v1.20.5 + github.com/SherClockHolmes/webpush-go v1.3.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 diff --git a/go.sum b/go.sum index 34fe33d84..874880c4e 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k= +github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= @@ -675,6 +677,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -712,6 +715,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -746,6 +750,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -765,6 +771,7 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -807,11 +814,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -821,6 +831,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -868,6 +880,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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(""), s) + + // Send Notification + resp, err := webpush.SendNotification([]byte("Test"), s, &webpush.Options{ + Subscriber: "example@example.com", + VAPIDPublicKey: "", + VAPIDPrivateKey: "", + 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 +} diff --git a/vendor/golang.org/x/crypto/hkdf/hkdf.go b/vendor/golang.org/x/crypto/hkdf/hkdf.go new file mode 100644 index 000000000..3bee66294 --- /dev/null +++ b/vendor/golang.org/x/crypto/hkdf/hkdf.go @@ -0,0 +1,95 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package hkdf implements the HMAC-based Extract-and-Expand Key Derivation +// Function (HKDF) as defined in RFC 5869. +// +// HKDF is a cryptographic key derivation function (KDF) with the goal of +// expanding limited input keying material into one or more cryptographically +// strong secret keys. +package hkdf + +import ( + "crypto/hmac" + "errors" + "hash" + "io" +) + +// Extract generates a pseudorandom key for use with Expand from an input secret +// and an optional independent salt. +// +// Only use this function if you need to reuse the extracted key with multiple +// Expand invocations and different context values. Most common scenarios, +// including the generation of multiple keys, should use New instead. +func Extract(hash func() hash.Hash, secret, salt []byte) []byte { + if salt == nil { + salt = make([]byte, hash().Size()) + } + extractor := hmac.New(hash, salt) + extractor.Write(secret) + return extractor.Sum(nil) +} + +type hkdf struct { + expander hash.Hash + size int + + info []byte + counter byte + + prev []byte + buf []byte +} + +func (f *hkdf) Read(p []byte) (int, error) { + // Check whether enough data can be generated + need := len(p) + remains := len(f.buf) + int(255-f.counter+1)*f.size + if remains < need { + return 0, errors.New("hkdf: entropy limit reached") + } + // Read any leftover from the buffer + n := copy(p, f.buf) + p = p[n:] + + // Fill the rest of the buffer + for len(p) > 0 { + if f.counter > 1 { + f.expander.Reset() + } + f.expander.Write(f.prev) + f.expander.Write(f.info) + f.expander.Write([]byte{f.counter}) + f.prev = f.expander.Sum(f.prev[:0]) + f.counter++ + + // Copy the new batch into p + f.buf = f.prev + n = copy(p, f.buf) + p = p[n:] + } + // Save leftovers for next run + f.buf = f.buf[n:] + + return need, nil +} + +// Expand returns a Reader, from which keys can be read, using the given +// pseudorandom key and optional context info, skipping the extraction step. +// +// The pseudorandomKey should have been generated by Extract, or be a uniformly +// random or pseudorandom cryptographically strong key. See RFC 5869, Section +// 3.3. Most common scenarios will want to use New instead. +func Expand(hash func() hash.Hash, pseudorandomKey, info []byte) io.Reader { + expander := hmac.New(hash, pseudorandomKey) + return &hkdf{expander, expander.Size(), info, 1, nil, nil} +} + +// New returns a Reader, from which keys can be read, using the given hash, +// secret, salt and context info. Salt and info can be nil. +func New(hash func() hash.Hash, secret, salt, info []byte) io.Reader { + prk := Extract(hash, secret, salt) + return Expand(hash, prk, info) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 4c57a75de..a97146255 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -84,6 +84,9 @@ github.com/Masterminds/semver/v3 # github.com/Masterminds/sprig/v3 v3.2.3 ## explicit; go 1.13 github.com/Masterminds/sprig/v3 +# github.com/SherClockHolmes/webpush-go v1.3.0 +## explicit; go 1.13 +github.com/SherClockHolmes/webpush-go # github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 ## explicit; go 1.13 github.com/asaskevich/govalidator @@ -1081,6 +1084,7 @@ golang.org/x/crypto/blowfish golang.org/x/crypto/chacha20 golang.org/x/crypto/curve25519 golang.org/x/crypto/ed25519 +golang.org/x/crypto/hkdf golang.org/x/crypto/internal/alias golang.org/x/crypto/internal/poly1305 golang.org/x/crypto/pbkdf2