BrainMinder/internal/cookies/cookies.go
2024-08-22 10:13:16 +02:00

151 lines
3.0 KiB
Go

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
}