package authproxy import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" ) // box seals and opens small JSON payloads with AES-256-GCM. The purpose // string is bound in as additional authenticated data so a token minted for // one use (e.g. OAuth state) can never be replayed as another (e.g. a // session cookie). type box struct { aead cipher.AEAD } func newBox(secret string) (*box, error) { key := sha256.Sum256([]byte(secret)) block, err := aes.NewCipher(key[:]) if err != nil { return nil, err } aead, err := cipher.NewGCM(block) if err != nil { return nil, err } return &box{aead: aead}, nil } func (b *box) seal(purpose string, v any) (string, error) { plain, err := json.Marshal(v) if err != nil { return "", err } nonce := make([]byte, b.aead.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } out := b.aead.Seal(nonce, nonce, plain, []byte(purpose)) return base64.RawURLEncoding.EncodeToString(out), nil } func (b *box) open(purpose, token string, v any) error { raw, err := base64.RawURLEncoding.DecodeString(token) if err != nil { return fmt.Errorf("malformed token: %w", err) } ns := b.aead.NonceSize() if len(raw) <= ns { return fmt.Errorf("malformed token: too short") } plain, err := b.aead.Open(nil, raw[:ns], raw[ns:], []byte(purpose)) if err != nil { return fmt.Errorf("token failed authentication: %w", err) } return json.Unmarshal(plain, v) } func randToken() string { buf := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, buf); err != nil { panic(err) // crypto/rand failure is unrecoverable } return hex.EncodeToString(buf) }