implement draft/webpush (#2205)

This commit is contained in:
Shivaram Lingamneni 2025-01-13 18:47:21 -08:00 committed by GitHub
parent efd3764337
commit 36e5451aa5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 2091 additions and 100 deletions

View 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
View file

@ -0,0 +1,6 @@
vendor/**
.DS_Store
*.out
*.swp

14
vendor/github.com/ergochat/webpush-go/v2/CHANGELOG.md generated vendored Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,65 @@
# webpush-go
[![GoDoc](https://godoc.org/github.com/ergochat/webpush-go?status.svg)](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
View 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
View 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
View 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
View 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
View 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
View file

@ -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