package aviso import ( "crypto/ecdh" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "strings" ) // ErrEmptyPrivateKey means Config.PrivateKey was empty. It is refused // rather than minted: a key generated at boot into local state is a // key lost at the next restore, and every browser then holds a // subscription nobody can sign for. var ErrEmptyPrivateKey = errors.New("aviso: Config.PrivateKey must not be empty; mint one with `go run amadan.net/rastrillo/aviso/cmd/aviso-key`") // ErrInvalidPrivateKey means Config.PrivateKey is not an unpadded // base64url 32-byte P-256 scalar in range. var ErrInvalidPrivateKey = errors.New("aviso: Config.PrivateKey is not an unpadded base64url 32-byte P-256 scalar") // GenerateKey mints a VAPID private key in Config.PrivateKey's format. // The public half is derived, never stored: one secret to provision. func GenerateKey() (string, error) { k, err := ecdh.P256().GenerateKey(rand.Reader) if err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(k.Bytes()), nil } // parsePrivateKey validates the scalar and derives the two things the // rest of the package needs from it: the uncompressed public point // (applicationServerKey on the browser side, the VAPID public key on // the wire) and a key id — SHA-256 of that point — stored on every // row so a rotated key is visible per subscription. func parsePrivateKey(s string) (pub, keyID string, err error) { if s == "" { return "", "", ErrEmptyPrivateKey } // Padding is refused, not tolerated: two encodings of one key // would be two strings an operator could paste, and only one of // them is what aviso-key printed. if strings.ContainsAny(s, "=+/") { return "", "", ErrInvalidPrivateKey } raw, err := base64.RawURLEncoding.DecodeString(s) if err != nil || len(raw) != 32 { return "", "", ErrInvalidPrivateKey } // The decoder tolerates newlines and non-zero trailing bits; only // the spelling that round-trips is the one aviso-key printed. if base64.RawURLEncoding.EncodeToString(raw) != s { return "", "", ErrInvalidPrivateKey } // NewPrivateKey rejects zero and out-of-range scalars. k, err := ecdh.P256().NewPrivateKey(raw) if err != nil { return "", "", ErrInvalidPrivateKey } point := k.PublicKey().Bytes() sum := sha256.Sum256(point) return base64.RawURLEncoding.EncodeToString(point), base64.RawURLEncoding.EncodeToString(sum[:]), nil }