Files
dokkku-google-auth/internal/authproxy/crypto.go
T
2026-07-15 07:49:11 -04:00

72 lines
1.7 KiB
Go

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)
}