mirror of
https://github.com/ergochat/ergo.git
synced 2025-12-20 02:00:11 -08:00
implement draft/webpush (#2205)
This commit is contained in:
parent
efd3764337
commit
36e5451aa5
44 changed files with 2091 additions and 100 deletions
13
vendor/github.com/ergochat/webpush-go/v2/.check-gofmt.sh
generated
vendored
Normal file
13
vendor/github.com/ergochat/webpush-go/v2/.check-gofmt.sh
generated
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
SOURCES="."
|
||||
|
||||
if [ "$1" = "--fix" ]; then
|
||||
exec gofmt -s -w $SOURCES
|
||||
fi
|
||||
|
||||
if [ -n "$(gofmt -s -l $SOURCES)" ]; then
|
||||
echo "Go code is not formatted correctly with \`gofmt -s\`:"
|
||||
gofmt -s -d $SOURCES
|
||||
exit 1
|
||||
fi
|
||||
6
vendor/github.com/ergochat/webpush-go/v2/.gitignore
generated
vendored
Normal file
6
vendor/github.com/ergochat/webpush-go/v2/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
vendor/**
|
||||
|
||||
.DS_Store
|
||||
*.out
|
||||
|
||||
*.swp
|
||||
14
vendor/github.com/ergochat/webpush-go/v2/CHANGELOG.md
generated
vendored
Normal file
14
vendor/github.com/ergochat/webpush-go/v2/CHANGELOG.md
generated
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Changelog
|
||||
All notable changes to webpush-go will be documented in this file.
|
||||
|
||||
## [2.0.0] - 2025-01-01
|
||||
|
||||
* Update the `Keys` struct definition to store `Auth` as `[16]byte` and `P256dh` as `*ecdh.PublicKey`
|
||||
* `Keys` can no longer be compared with `==`; use `(*Keys.Equal)` instead
|
||||
* The JSON representation has not changed and is backwards and forwards compatible with v1
|
||||
* `DecodeSubscriptionKeys` is a helper to decode base64-encoded auth and p256dh parameters into a `Keys`, with validation
|
||||
* Update the `VAPIDKeys` struct to contain a `(*ecdsa.PrivateKey)`
|
||||
* `VAPIDKeys` can no longer be compared with `==`; use `(*VAPIDKeys).Equal` instead
|
||||
* The JSON representation is now a JSON string containing the PEM of the PKCS8-encoded private key
|
||||
* To parse the legacy representation (raw bytes of the private key encoded in base64), use `DecodeLegacyVAPIDPrivateKey`
|
||||
* Renamed `SendNotificationWithContext` to `SendNotification`, removing the earlier `SendNotification` API. (Pass `context.Background()` as the context to restore the former behavior.)
|
||||
21
vendor/github.com/ergochat/webpush-go/v2/LICENSE
generated
vendored
Normal file
21
vendor/github.com/ergochat/webpush-go/v2/LICENSE
generated
vendored
Normal file
|
|
@ -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.
|
||||
6
vendor/github.com/ergochat/webpush-go/v2/Makefile
generated
vendored
Normal file
6
vendor/github.com/ergochat/webpush-go/v2/Makefile
generated
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.PHONY: test
|
||||
|
||||
test:
|
||||
go test .
|
||||
go vet .
|
||||
./.check-gofmt.sh
|
||||
65
vendor/github.com/ergochat/webpush-go/v2/README.md
generated
vendored
Normal file
65
vendor/github.com/ergochat/webpush-go/v2/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# webpush-go
|
||||
|
||||
[](https://godoc.org/github.com/ergochat/webpush-go)
|
||||
|
||||
Web Push API Encryption with VAPID support.
|
||||
|
||||
This library is a fork of [SherClockHolmes/webpush-go](https://github.com/SherClockHolmes/webpush-go).
|
||||
|
||||
```bash
|
||||
go get -u github.com/ergochat/webpush-go/v2
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
For a full example, refer to the code in the [example](example/) directory.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
webpush "github.com/ergochat/webpush-go/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Decode subscription
|
||||
s := &webpush.Subscription{}
|
||||
json.Unmarshal([]byte("<YOUR_SUBSCRIPTION>"), s)
|
||||
vapidKeys := new(webpush.VAPIDKeys)
|
||||
json.Unmarshal([]byte("<YOUR_VAPID_KEYS">), vapidKeys)
|
||||
|
||||
// Send Notification
|
||||
resp, err := webpush.SendNotification([]byte("Test"), s, &webpush.Options{
|
||||
Subscriber: "example@example.com",
|
||||
VAPIDKeys: vapidKeys,
|
||||
TTL: 3600, // seconds
|
||||
})
|
||||
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
|
||||
vapidKeys, err := webpush.GenerateVAPIDKeys()
|
||||
if err != nil {
|
||||
// TODO: Handle error
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
1. Install [Go 1.20+](https://golang.org/)
|
||||
2. `go mod vendor`
|
||||
3. `go test`
|
||||
|
||||
#### For other language implementations visit:
|
||||
|
||||
[WebPush Libs](https://github.com/web-push-libs)
|
||||
76
vendor/github.com/ergochat/webpush-go/v2/legacy.go
generated
vendored
Normal file
76
vendor/github.com/ergochat/webpush-go/v2/legacy.go
generated
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package webpush
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// ecdhPublicKeyToECDSA converts an ECDH key to an ECDSA key.
|
||||
// This is deprecated as per https://github.com/golang/go/issues/63963
|
||||
// but we need to do it in order to parse the legacy private key format.
|
||||
func ecdhPublicKeyToECDSA(key *ecdh.PublicKey) (*ecdsa.PublicKey, error) {
|
||||
rawKey := key.Bytes()
|
||||
switch key.Curve() {
|
||||
case ecdh.P256():
|
||||
return &ecdsa.PublicKey{
|
||||
Curve: elliptic.P256(),
|
||||
X: big.NewInt(0).SetBytes(rawKey[1:33]),
|
||||
Y: big.NewInt(0).SetBytes(rawKey[33:]),
|
||||
}, nil
|
||||
case ecdh.P384():
|
||||
return &ecdsa.PublicKey{
|
||||
Curve: elliptic.P384(),
|
||||
X: big.NewInt(0).SetBytes(rawKey[1:49]),
|
||||
Y: big.NewInt(0).SetBytes(rawKey[49:]),
|
||||
}, nil
|
||||
case ecdh.P521():
|
||||
return &ecdsa.PublicKey{
|
||||
Curve: elliptic.P521(),
|
||||
X: big.NewInt(0).SetBytes(rawKey[1:67]),
|
||||
Y: big.NewInt(0).SetBytes(rawKey[67:]),
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot convert non-NIST *ecdh.PublicKey to *ecdsa.PublicKey")
|
||||
}
|
||||
}
|
||||
|
||||
func ecdhPrivateKeyToECDSA(key *ecdh.PrivateKey) (*ecdsa.PrivateKey, error) {
|
||||
// see https://github.com/golang/go/issues/63963
|
||||
pubKey, err := ecdhPublicKeyToECDSA(key.PublicKey())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting PublicKey part of *ecdh.PrivateKey: %w", err)
|
||||
}
|
||||
return &ecdsa.PrivateKey{
|
||||
PublicKey: *pubKey,
|
||||
D: big.NewInt(0).SetBytes(key.Bytes()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecodeLegacyVAPIDPrivateKey decodes the legacy string private key format
|
||||
// returned by GenerateVAPIDKeys in v1.
|
||||
func DecodeLegacyVAPIDPrivateKey(key string) (*VAPIDKeys, error) {
|
||||
bytes, err := decodeSubscriptionKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ecdhPrivKey, err := ecdh.P256().NewPrivateKey(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ecdsaPrivKey, err := ecdhPrivateKeyToECDSA(ecdhPrivKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicKey := base64.RawURLEncoding.EncodeToString(ecdhPrivKey.PublicKey().Bytes())
|
||||
return &VAPIDKeys{
|
||||
privateKey: ecdsaPrivKey,
|
||||
publicKey: publicKey,
|
||||
}, nil
|
||||
}
|
||||
26
vendor/github.com/ergochat/webpush-go/v2/urgency.go
generated
vendored
Normal file
26
vendor/github.com/ergochat/webpush-go/v2/urgency.go
generated
vendored
Normal file
|
|
@ -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
|
||||
}
|
||||
177
vendor/github.com/ergochat/webpush-go/v2/vapid.go
generated
vendored
Normal file
177
vendor/github.com/ergochat/webpush-go/v2/vapid.go
generated
vendored
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
package webpush
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwt "github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// VAPIDKeys is a public-private keypair for use in VAPID.
|
||||
// It marshals to a JSON string containing the PEM of the PKCS8
|
||||
// of the private key.
|
||||
type VAPIDKeys struct {
|
||||
privateKey *ecdsa.PrivateKey
|
||||
publicKey string // raw bytes encoding in urlsafe base64, as per RFC
|
||||
}
|
||||
|
||||
// PublicKeyString returns the base64url-encoded uncompressed public key of the keypair,
|
||||
// as defined in RFC8292.
|
||||
func (v *VAPIDKeys) PublicKeyString() string {
|
||||
return v.publicKey
|
||||
}
|
||||
|
||||
// PrivateKey returns the private key of the keypair.
|
||||
func (v *VAPIDKeys) PrivateKey() *ecdsa.PrivateKey {
|
||||
return v.privateKey
|
||||
}
|
||||
|
||||
// Equal compares two VAPIDKeys for equality.
|
||||
func (v *VAPIDKeys) Equal(o *VAPIDKeys) bool {
|
||||
return v.privateKey.Equal(o.privateKey)
|
||||
}
|
||||
|
||||
var _ json.Marshaler = (*VAPIDKeys)(nil)
|
||||
var _ json.Unmarshaler = (*VAPIDKeys)(nil)
|
||||
|
||||
// MarshalJSON implements json.Marshaler, allowing serialization to JSON.
|
||||
func (v *VAPIDKeys) MarshalJSON() ([]byte, error) {
|
||||
pkcs8bytes, err := x509.MarshalPKCS8PrivateKey(v.privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pemBlock := pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: pkcs8bytes,
|
||||
}
|
||||
pemBytes := pem.EncodeToMemory(&pemBlock)
|
||||
if pemBytes == nil {
|
||||
return nil, fmt.Errorf("could not encode VAPID keys as PEM")
|
||||
}
|
||||
return json.Marshal(string(pemBytes))
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Unmarshaler, allowing deserialization from JSON.
|
||||
func (v *VAPIDKeys) UnmarshalJSON(b []byte) error {
|
||||
var pemKey string
|
||||
if err := json.Unmarshal(b, &pemKey); err != nil {
|
||||
return err
|
||||
}
|
||||
pemBlock, _ := pem.Decode([]byte(pemKey))
|
||||
if pemBlock == nil {
|
||||
return fmt.Errorf("could not decode PEM block with VAPID keys")
|
||||
}
|
||||
privKey, err := x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateKey, ok := privKey.(*ecdsa.PrivateKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("Invalid type of private key %T", privateKey)
|
||||
}
|
||||
if privateKey.Curve != elliptic.P256() {
|
||||
return fmt.Errorf("Invalid curve for private key %v", privateKey.Curve)
|
||||
}
|
||||
publicKeyStr, err := makePublicKeyString(privateKey)
|
||||
if err != nil {
|
||||
return err // should not be possible since we confirmed P256 already
|
||||
}
|
||||
|
||||
// success
|
||||
v.privateKey = privateKey
|
||||
v.publicKey = publicKeyStr
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateVAPIDKeys generates a VAPID keypair (an ECDSA keypair on
|
||||
// the P-256 curve).
|
||||
func GenerateVAPIDKeys() (result *VAPIDKeys, err error) {
|
||||
private, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
pubKeyECDH, err := private.PublicKey.ECDH()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
publicKey := base64.RawURLEncoding.EncodeToString(pubKeyECDH.Bytes())
|
||||
|
||||
return &VAPIDKeys{
|
||||
privateKey: private,
|
||||
publicKey: publicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ECDSAToVAPIDKeys wraps an existing ecdsa.PrivateKey in VAPIDKeys for use in
|
||||
// VAPID header signing.
|
||||
func ECDSAToVAPIDKeys(privKey *ecdsa.PrivateKey) (result *VAPIDKeys, err error) {
|
||||
if privKey.Curve != elliptic.P256() {
|
||||
return nil, fmt.Errorf("Invalid curve for private key %v", privKey.Curve)
|
||||
}
|
||||
publicKeyString, err := makePublicKeyString(privKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &VAPIDKeys{
|
||||
privateKey: privKey,
|
||||
publicKey: publicKeyString,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func makePublicKeyString(privKey *ecdsa.PrivateKey) (result string, err error) {
|
||||
// to get the raw bytes we have to convert the public key to *ecdh.PublicKey
|
||||
// this type assertion (from the crypto.PublicKey returned by (*ecdsa.PrivateKey).Public()
|
||||
// to *ecdsa.PublicKey) cannot fail:
|
||||
publicKey, err := privKey.Public().(*ecdsa.PublicKey).ECDH()
|
||||
if err != nil {
|
||||
return // should not be possible if we confirmed P256 already
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(publicKey.Bytes()), nil
|
||||
}
|
||||
|
||||
// getVAPIDAuthorizationHeader
|
||||
func getVAPIDAuthorizationHeader(
|
||||
endpoint string,
|
||||
subscriber string,
|
||||
vapidKeys *VAPIDKeys,
|
||||
expiration time.Time,
|
||||
) (string, error) {
|
||||
if expiration.IsZero() {
|
||||
expiration = time.Now().Add(time.Hour * 12)
|
||||
}
|
||||
|
||||
// Create the JWT token
|
||||
subURL, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Unless subscriber is an HTTPS URL, assume an e-mail address
|
||||
if !strings.HasPrefix(subscriber, "https:") && !strings.HasPrefix(subscriber, "mailto:") {
|
||||
subscriber = "mailto:" + subscriber
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
|
||||
"aud": subURL.Scheme + "://" + subURL.Host,
|
||||
"exp": expiration.Unix(),
|
||||
"sub": subscriber,
|
||||
})
|
||||
|
||||
// Sign token with private key
|
||||
jwtString, err := token.SignedString(vapidKeys.privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return "vapid t=" + jwtString + ", k=" + vapidKeys.publicKey, nil
|
||||
}
|
||||
323
vendor/github.com/ergochat/webpush-go/v2/webpush.go
generated
vendored
Normal file
323
vendor/github.com/ergochat/webpush-go/v2/webpush.go
generated
vendored
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
package webpush
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/ecdh"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
const MaxRecordSize uint32 = 4096
|
||||
|
||||
var (
|
||||
ErrRecordSizeTooSmall = errors.New("record size too small for message")
|
||||
|
||||
invalidAuthKeyLength = errors.New("invalid auth key length (must be 16)")
|
||||
|
||||
defaultHTTPClient = &http.Client{}
|
||||
)
|
||||
|
||||
// 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, in seconds
|
||||
Urgency Urgency // Set the Urgency header to change a message priority (Optional)
|
||||
VAPIDKeys *VAPIDKeys // VAPID public-private keypair to generate the VAPID Authorization header
|
||||
VapidExpiration time.Time // optional expiration for VAPID JWT token (defaults to now + 12 hours)
|
||||
}
|
||||
|
||||
// Keys represents a subscription's keys (its ECDH public key on the P-256 curve
|
||||
// and its 16-byte authentication secret).
|
||||
type Keys struct {
|
||||
Auth [16]byte
|
||||
P256dh *ecdh.PublicKey
|
||||
}
|
||||
|
||||
// Equal compares two Keys for equality.
|
||||
func (k *Keys) Equal(o Keys) bool {
|
||||
return k.Auth == o.Auth && k.P256dh.Equal(o.P256dh)
|
||||
}
|
||||
|
||||
var _ json.Marshaler = (*Keys)(nil)
|
||||
var _ json.Unmarshaler = (*Keys)(nil)
|
||||
|
||||
type marshaledKeys struct {
|
||||
Auth string `json:"auth"`
|
||||
P256dh string `json:"p256dh"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Marshaler, allowing serialization to JSON.
|
||||
func (k *Keys) MarshalJSON() ([]byte, error) {
|
||||
m := marshaledKeys{
|
||||
Auth: base64.RawStdEncoding.EncodeToString(k.Auth[:]),
|
||||
P256dh: base64.RawStdEncoding.EncodeToString(k.P256dh.Bytes()),
|
||||
}
|
||||
return json.Marshal(&m)
|
||||
}
|
||||
|
||||
// MarshalJSON implements json.Unmarshaler, allowing deserialization from JSON.
|
||||
func (k *Keys) UnmarshalJSON(b []byte) (err error) {
|
||||
var m marshaledKeys
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return err
|
||||
}
|
||||
authBytes, err := decodeSubscriptionKey(m.Auth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(authBytes) != 16 {
|
||||
return fmt.Errorf("invalid auth bytes length %d (must be 16)", len(authBytes))
|
||||
}
|
||||
copy(k.Auth[:], authBytes)
|
||||
rawDHKey, err := decodeSubscriptionKey(m.P256dh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k.P256dh, err = ecdh.P256().NewPublicKey(rawDHKey)
|
||||
return err
|
||||
}
|
||||
|
||||
// DecodeSubscriptionKeys decodes and validates a base64-encoded pair of subscription keys
|
||||
// (the authentication secret and ECDH public key).
|
||||
func DecodeSubscriptionKeys(auth, p256dh string) (keys Keys, err error) {
|
||||
authBytes, err := decodeSubscriptionKey(auth)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(authBytes) != 16 {
|
||||
err = invalidAuthKeyLength
|
||||
return
|
||||
}
|
||||
copy(keys.Auth[:], authBytes)
|
||||
dhBytes, err := decodeSubscriptionKey(p256dh)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
keys.P256dh, err = ecdh.P256().NewPublicKey(dhBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Subscription represents a PushSubscription object from the Push API
|
||||
type Subscription struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Keys Keys `json:"keys"`
|
||||
}
|
||||
|
||||
// SendNotification sends a push notification to a subscription's endpoint,
|
||||
// applying encryption (RFC 8291) and adding a VAPID header (RFC 8292).
|
||||
func SendNotification(ctx context.Context, message []byte, s *Subscription, options *Options) (*http.Response, error) {
|
||||
// Compose message body (RFC8291 encryption of the message)
|
||||
body, err := EncryptNotification(message, s.Keys, options.RecordSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get VAPID Authorization header
|
||||
vapidAuthHeader, err := getVAPIDAuthorizationHeader(
|
||||
s.Endpoint,
|
||||
options.Subscriber,
|
||||
options.VAPIDKeys,
|
||||
options.VapidExpiration,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Compose and send the HTTP request
|
||||
return sendNotification(ctx, s.Endpoint, options, vapidAuthHeader, body)
|
||||
}
|
||||
|
||||
// EncryptNotification implements the encryption algorithm specified by RFC 8291 for web push
|
||||
// (RFC 8188's aes128gcm content-encoding, with the key material derived from
|
||||
// elliptic curve Diffie-Hellman over the P-256 curve).
|
||||
func EncryptNotification(message []byte, keys Keys, recordSize uint32) ([]byte, error) {
|
||||
// Get the record size
|
||||
if recordSize == 0 {
|
||||
recordSize = MaxRecordSize
|
||||
} else if recordSize < 128 {
|
||||
return nil, ErrRecordSizeTooSmall
|
||||
}
|
||||
|
||||
// Allocate buffer to hold the eventual message
|
||||
// [ header block ] [ ciphertext ] [ 16 byte AEAD tag ], totaling RecordSize bytes
|
||||
// the ciphertext is the encryption of: [ message ] [ \x02 ] [ 0 or more \x00 as needed ]
|
||||
recordBuf := make([]byte, recordSize)
|
||||
// remainingBuf tracks our current writing position in recordBuf:
|
||||
remainingBuf := recordBuf
|
||||
|
||||
// Application server key pairs (single use)
|
||||
localPrivateKey, err := ecdh.P256().GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
localPublicKey := localPrivateKey.PublicKey()
|
||||
|
||||
// Encryption Content-Coding Header
|
||||
// +-----------+--------+-----------+---------------+
|
||||
// | salt (16) | rs (4) | idlen (1) | keyid (idlen) |
|
||||
// +-----------+--------+-----------+---------------+
|
||||
// in our case the keyid is localPublicKey.Bytes(), so 65 bytes
|
||||
// First, generate the salt
|
||||
_, err = rand.Read(remainingBuf[:16])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
salt := remainingBuf[:16]
|
||||
remainingBuf = remainingBuf[16:]
|
||||
binary.BigEndian.PutUint32(remainingBuf[:], recordSize)
|
||||
remainingBuf = remainingBuf[4:]
|
||||
localPublicKeyBytes := localPublicKey.Bytes()
|
||||
remainingBuf[0] = byte(len(localPublicKeyBytes))
|
||||
remainingBuf = remainingBuf[1:]
|
||||
copy(remainingBuf[:], localPublicKeyBytes)
|
||||
remainingBuf = remainingBuf[len(localPublicKeyBytes):]
|
||||
|
||||
// Combine application keys with receiver's EC public key to derive ECDH shared secret
|
||||
sharedECDHSecret, err := localPrivateKey.ECDH(keys.P256dh)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("deriving shared secret: %w", err)
|
||||
}
|
||||
|
||||
// ikm
|
||||
prkInfoBuf := bytes.NewBuffer([]byte("WebPush: info\x00"))
|
||||
prkInfoBuf.Write(keys.P256dh.Bytes())
|
||||
prkInfoBuf.Write(localPublicKey.Bytes())
|
||||
|
||||
prkHKDF := hkdf.New(sha256.New, sharedECDHSecret, keys.Auth[:], 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(sha256.New, 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(sha256.New, 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
|
||||
}
|
||||
|
||||
// need 1 byte for the 0x02 delimiter, 16 bytes for the AEAD tag
|
||||
if len(remainingBuf) < len(message)+17 {
|
||||
return nil, ErrRecordSizeTooSmall
|
||||
}
|
||||
// Copy the message plaintext into the buffer
|
||||
copy(remainingBuf[:], message[:])
|
||||
// The plaintext to be encrypted will include the padding delimiter and the padding;
|
||||
// cut off the final 16 bytes that are reserved for the AEAD tag
|
||||
plaintext := remainingBuf[:len(remainingBuf)-16]
|
||||
remainingBuf = remainingBuf[len(message):]
|
||||
// Add padding delimiter
|
||||
remainingBuf[0] = '\x02'
|
||||
remainingBuf = remainingBuf[1:]
|
||||
// The rest of the buffer is already zero-padded
|
||||
|
||||
// Encipher the plaintext in place, then add the AEAD tag at the end.
|
||||
// "To reuse plaintext's storage for the encrypted output, use plaintext[:0]
|
||||
// as dst. Otherwise, the remaining capacity of dst must not overlap plaintext."
|
||||
gcm.Seal(plaintext[:0], nonce, plaintext, nil)
|
||||
|
||||
return recordBuf, nil
|
||||
}
|
||||
|
||||
func sendNotification(ctx context.Context, endpoint string, options *Options, vapidAuthHeader string, body []byte) (*http.Response, error) {
|
||||
// POST request
|
||||
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ctx != nil {
|
||||
req = req.WithContext(ctx)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Encoding", "aes128gcm")
|
||||
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))
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", vapidAuthHeader)
|
||||
|
||||
// Send the request
|
||||
var client HTTPClient
|
||||
if options.HTTPClient != nil {
|
||||
client = options.HTTPClient
|
||||
} else {
|
||||
client = defaultHTTPClient
|
||||
}
|
||||
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
// decodeSubscriptionKey decodes a base64 subscription key.
|
||||
func decodeSubscriptionKey(key string) ([]byte, error) {
|
||||
key = strings.TrimRight(key, "=")
|
||||
|
||||
if strings.IndexByte(key, '+') != -1 || strings.IndexByte(key, '/') != -1 {
|
||||
return base64.RawStdEncoding.DecodeString(key)
|
||||
}
|
||||
return base64.RawURLEncoding.DecodeString(key)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
95
vendor/golang.org/x/crypto/hkdf/hkdf.go
generated
vendored
Normal file
95
vendor/golang.org/x/crypto/hkdf/hkdf.go
generated
vendored
Normal file
|
|
@ -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)
|
||||
}
|
||||
4
vendor/modules.txt
vendored
4
vendor/modules.txt
vendored
|
|
@ -22,6 +22,9 @@ github.com/ergochat/irc-go/ircfmt
|
|||
github.com/ergochat/irc-go/ircmsg
|
||||
github.com/ergochat/irc-go/ircreader
|
||||
github.com/ergochat/irc-go/ircutils
|
||||
# github.com/ergochat/webpush-go/v2 v2.0.0-rc1
|
||||
## explicit; go 1.20
|
||||
github.com/ergochat/webpush-go/v2
|
||||
# github.com/go-sql-driver/mysql v1.7.0
|
||||
## explicit; go 1.13
|
||||
github.com/go-sql-driver/mysql
|
||||
|
|
@ -83,6 +86,7 @@ github.com/xdg-go/scram
|
|||
## explicit; go 1.20
|
||||
golang.org/x/crypto/bcrypt
|
||||
golang.org/x/crypto/blowfish
|
||||
golang.org/x/crypto/hkdf
|
||||
golang.org/x/crypto/pbkdf2
|
||||
golang.org/x/crypto/sha3
|
||||
# golang.org/x/sys v0.22.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue