package cookies import ( "crypto/aes" "crypto/cipher" "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "fmt" "io" "net/http" "strings" ) var ( ErrValueTooLong = errors.New("cookie value too long") ErrInvalidValue = errors.New("invalid cookie value") ) func Write(w http.ResponseWriter, cookie http.Cookie) error { cookie.Value = base64.URLEncoding.EncodeToString([]byte(cookie.Value)) if len(cookie.String()) > 4096 { return ErrValueTooLong } http.SetCookie(w, &cookie) return nil } func Read(r *http.Request, name string) (string, error) { cookie, err := r.Cookie(name) if err != nil { return "", err } value, err := base64.URLEncoding.DecodeString(cookie.Value) if err != nil { return "", ErrInvalidValue } return string(value), nil } func WriteSigned(w http.ResponseWriter, cookie http.Cookie, secretKey string) error { mac := hmac.New(sha256.New, []byte(secretKey)) mac.Write([]byte(cookie.Name)) mac.Write([]byte(cookie.Value)) signature := mac.Sum(nil) cookie.Value = string(signature) + cookie.Value return Write(w, cookie) } func ReadSigned(r *http.Request, name string, secretKey string) (string, error) { signedValue, err := Read(r, name) if err != nil { return "", err } if len(signedValue) < sha256.Size { return "", ErrInvalidValue } signature := signedValue[:sha256.Size] value := signedValue[sha256.Size:] mac := hmac.New(sha256.New, []byte(secretKey)) mac.Write([]byte(name)) mac.Write([]byte(value)) expectedSignature := mac.Sum(nil) if !hmac.Equal([]byte(signature), expectedSignature) { return "", ErrInvalidValue } return value, nil } func WriteEncrypted(w http.ResponseWriter, cookie http.Cookie, secretKey string) error { block, err := aes.NewCipher([]byte(secretKey)) if err != nil { return err } aesGCM, err := cipher.NewGCM(block) if err != nil { return err } nonce := make([]byte, aesGCM.NonceSize()) _, err = io.ReadFull(rand.Reader, nonce) if err != nil { return err } plaintext := fmt.Sprintf("%s:%s", cookie.Name, cookie.Value) encryptedValue := aesGCM.Seal(nonce, nonce, []byte(plaintext), nil) cookie.Value = string(encryptedValue) return Write(w, cookie) } func ReadEncrypted(r *http.Request, name string, secretKey string) (string, error) { encryptedValue, err := Read(r, name) if err != nil { return "", err } block, err := aes.NewCipher([]byte(secretKey)) if err != nil { return "", err } aesGCM, err := cipher.NewGCM(block) if err != nil { return "", err } nonceSize := aesGCM.NonceSize() if len(encryptedValue) < nonceSize { return "", ErrInvalidValue } nonce := encryptedValue[:nonceSize] ciphertext := encryptedValue[nonceSize:] plaintext, err := aesGCM.Open(nil, []byte(nonce), []byte(ciphertext), nil) if err != nil { return "", ErrInvalidValue } expectedName, value, ok := strings.Cut(string(plaintext), ":") if !ok { return "", ErrInvalidValue } if expectedName != name { return "", ErrInvalidValue } return value, nil }