rastrillo / aviso Public

Clone
git clone https://amadan.net/rastrillo/aviso

Plain git — no account needed to clone.

Download

Download this file

1package aviso
2
3import (
4 "crypto/ecdh"
5 "crypto/rand"
6 "crypto/sha256"
7 "encoding/base64"
8 "errors"
9 "strings"
10)
11
12// ErrEmptyPrivateKey means Config.PrivateKey was empty. It is refused
13// rather than minted: a key generated at boot into local state is a
14// key lost at the next restore, and every browser then holds a
15// subscription nobody can sign for.
16var ErrEmptyPrivateKey = errors.New("aviso: Config.PrivateKey must not be empty; mint one with `go run amadan.net/rastrillo/aviso/cmd/aviso-key`")
17
18// ErrInvalidPrivateKey means Config.PrivateKey is not an unpadded
19// base64url 32-byte P-256 scalar in range.
20var ErrInvalidPrivateKey = errors.New("aviso: Config.PrivateKey is not an unpadded base64url 32-byte P-256 scalar")
21
22// GenerateKey mints a VAPID private key in Config.PrivateKey's format.
23// The public half is derived, never stored: one secret to provision.
24func GenerateKey() (string, error) {
25 k, err := ecdh.P256().GenerateKey(rand.Reader)
26 if err != nil {
27 return "", err
28 }
29 return base64.RawURLEncoding.EncodeToString(k.Bytes()), nil
30}
31
32// parsePrivateKey validates the scalar and derives the two things the
33// rest of the package needs from it: the uncompressed public point
34// (applicationServerKey on the browser side, the VAPID public key on
35// the wire) and a key id — SHA-256 of that point — stored on every
36// row so a rotated key is visible per subscription.
37func parsePrivateKey(s string) (pub, keyID string, err error) {
38 if s == "" {
39 return "", "", ErrEmptyPrivateKey
40 }
41 // Padding is refused, not tolerated: two encodings of one key
42 // would be two strings an operator could paste, and only one of
43 // them is what aviso-key printed.
44 if strings.ContainsAny(s, "=+/") {
45 return "", "", ErrInvalidPrivateKey
46 }
47 raw, err := base64.RawURLEncoding.DecodeString(s)
48 if err != nil || len(raw) != 32 {
49 return "", "", ErrInvalidPrivateKey
50 }
51 // The decoder tolerates newlines and non-zero trailing bits; only
52 // the spelling that round-trips is the one aviso-key printed.
53 if base64.RawURLEncoding.EncodeToString(raw) != s {
54 return "", "", ErrInvalidPrivateKey
55 }
56 // NewPrivateKey rejects zero and out-of-range scalars.
57 k, err := ecdh.P256().NewPrivateKey(raw)
58 if err != nil {
59 return "", "", ErrInvalidPrivateKey
60 }
61 point := k.PublicKey().Bytes()
62 sum := sha256.Sum256(point)
63 return base64.RawURLEncoding.EncodeToString(point),
64 base64.RawURLEncoding.EncodeToString(sum[:]), nil
65}
66