internal/push/token.go
77 lines · 2198 bytes
5 symbols in this file
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}