Skip to content

Instantly share code, notes, and snippets.

@alexedwards
Created September 28, 2022 10:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexedwards/6d50c564dc597c243ce8b4aa2684d28f to your computer and use it in GitHub Desktop.
Save alexedwards/6d50c564dc597c243ce8b4aa2684d28f to your computer and use it in GitHub Desktop.
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 []byte) error {
mac := hmac.New(sha256.New, 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 []byte) (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, 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 []byte) error {
block, err := aes.NewCipher(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 []byte) (string, error) {
encryptedValue, err := Read(r, name)
if err != nil {
return "", err
}
block, err := aes.NewCipher(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
}
package main
import (
"bytes"
"encoding/gob"
"encoding/hex"
"errors"
"fmt"
"log"
"net/http"
"strings"
"example.com/example-project/internal/cookies"
)
var secret []byte
type User struct {
Name string
Age int
}
func main() {
gob.Register(&User{})
var err error
secret, err = hex.DecodeString("13d6b4dff8f84a10851021ec8608f814570d562c92fe6b5ec4c9f595bcb3234b")
if err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
mux.HandleFunc("/set", setCookieHandler)
mux.HandleFunc("/get", getCookieHandler)
log.Print("Listening...")
err = http.ListenAndServe(":3000", mux)
if err != nil {
log.Fatal(err)
}
}
func setCookieHandler(w http.ResponseWriter, r *http.Request) {
user := User{Name: "Alice", Age: 21}
var buf bytes.Buffer
err := gob.NewEncoder(&buf).Encode(&user)
if err != nil {
log.Println(err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
cookie := http.Cookie{
Name: "exampleCookie",
Value: buf.String(),
Path: "/",
MaxAge: 3600,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
}
err = cookies.WriteEncrypted(w, cookie, secret)
if err != nil {
log.Println(err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
w.Write([]byte("cookie set!"))
}
func getCookieHandler(w http.ResponseWriter, r *http.Request) {
gobEncodedValue, err := cookies.ReadEncrypted(r, "exampleCookie", secret)
if err != nil {
switch {
case errors.Is(err, http.ErrNoCookie):
http.Error(w, "cookie not found", http.StatusBadRequest)
case errors.Is(err, cookies.ErrInvalidValue):
http.Error(w, "invalid cookie", http.StatusBadRequest)
default:
log.Println(err)
http.Error(w, "server error", http.StatusInternalServerError)
}
return
}
var user User
reader := strings.NewReader(gobEncodedValue)
if err := gob.NewDecoder(reader).Decode(&user); err != nil {
log.Println(err)
http.Error(w, "server error", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "Name: %q\n", user.Name)
fmt.Fprintf(w, "Age: %d\n", user.Age)
}
@Sirneij
Copy link

Sirneij commented May 31, 2023

This is helpful. How do we save the generated cookie in a redis store?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment