internal/push/token.go

bd5cf5d7d1f34fa780660fd7562b9ffd9746ee27
gitbay/internal/push/token.go history · blame · raw

77 lines · 2198 bytes

 1// Package push delivers activity notices to Apple devices over APNs: the
 2// third delivery route beside the inbox row and the activity mail, with
 3// the bounded-retry discipline the mail queue and webhook deliverer use.
 4package push
 5
 6import (
 7	"crypto/ecdsa"
 8	"crypto/rand"
 9	"crypto/sha256"
10	"encoding/base64"
11	"encoding/json"
12	"sync"
13	"time"
14)
15
16// tokenLifetime is how long a provider token is reused. APNs accepts one
17// for an hour and answers TooManyProviderTokenUpdates if they are minted
18// faster than roughly once every twenty minutes, so the useful window is
19// between the two.
20const tokenLifetime = 50 * time.Minute
21
22type tokenSource struct {
23	key    *ecdsa.PrivateKey
24	keyID  string
25	teamID string
26	now    func() time.Time
27
28	mu     sync.Mutex
29	cached string
30	issued time.Time
31}
32
33func newTokenSource(key *ecdsa.PrivateKey, keyID, teamID string) *tokenSource {
34	return &tokenSource{key: key, keyID: keyID, teamID: teamID, now: time.Now}
35}
36
37// token returns the cached provider token, minting a new one when the old
38// one is near its end.
39func (t *tokenSource) token() (string, error) {
40	t.mu.Lock()
41	defer t.mu.Unlock()
42	now := t.now()
43	if t.cached != "" && now.Sub(t.issued) < tokenLifetime {
44		return t.cached, nil
45	}
46	tok, err := t.sign(now)
47	if err != nil {
48		return "", err
49	}
50	t.cached, t.issued = tok, now
51	return tok, nil
52}
53
54func (t *tokenSource) sign(now time.Time) (string, error) {
55	header, err := json.Marshal(map[string]string{"alg": "ES256", "kid": t.keyID})
56	if err != nil {
57		return "", err
58	}
59	claims, err := json.Marshal(map[string]any{"iss": t.teamID, "iat": now.Unix()})
60	if err != nil {
61		return "", err
62	}
63	enc := base64.RawURLEncoding
64	signing := enc.EncodeToString(header) + "." + enc.EncodeToString(claims)
65	sum := sha256.Sum256([]byte(signing))
66	r, s, err := ecdsa.Sign(rand.Reader, t.key, sum[:])
67	if err != nil {
68		return "", err
69	}
70	// JWS wants the raw pair, each left-padded to the curve's byte size —
71	// not ecdsa.SignASN1's DER. A DER signature is well-formed ECDSA and
72	// is rejected by every JWT verifier, APNs included.
73	sig := make([]byte, 64)
74	r.FillBytes(sig[:32])
75	s.FillBytes(sig[32:])
76	return signing + "." + enc.EncodeToString(sig), nil
77}