2025-01-24 00:47:30 +00:00
|
|
|
package webpush
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/ecdsa"
|
|
|
|
"crypto/elliptic"
|
|
|
|
"crypto/rand"
|
|
|
|
"encoding/base64"
|
|
|
|
"math/big"
|
|
|
|
"net/url"
|
2025-01-27 11:08:13 +00:00
|
|
|
"strings"
|
2025-01-24 00:47:30 +00:00
|
|
|
"time"
|
|
|
|
|
2025-01-27 11:08:13 +00:00
|
|
|
"github.com/golang-jwt/jwt/v5"
|
2025-01-24 00:47:30 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2025-01-27 11:08:13 +00:00
|
|
|
// Unless subscriber is an HTTPS URL, assume an e-mail address
|
|
|
|
if !strings.HasPrefix(subscriber, "https:") {
|
|
|
|
subscriber = "mailto:" + subscriber
|
|
|
|
}
|
|
|
|
|
2025-01-24 00:47:30 +00:00
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
|
2025-01-27 11:08:13 +00:00
|
|
|
"aud": subURL.Scheme + "://" + subURL.Host,
|
|
|
|
"exp": time.Now().Add(time.Hour * 12).Unix(),
|
|
|
|
"sub": subscriber,
|
2025-01-24 00:47:30 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2025-01-27 11:08:13 +00:00
|
|
|
return "vapid t=" + jwtString + ", k=" + base64.RawURLEncoding.EncodeToString(pubKey), nil
|
2025-01-24 00:47:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|