151 lines
3.0 KiB
Go
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
|
||
|
}
|