Introduction to the

A cookie is a mechanism used to transfer small amounts of data between a Web client (typically a browser) and a server. The cookie is generated by the server and sent to the client for preservation. The client will bring the cookie with each subsequent request. Cookies are now more or less abused. Many companies use cookies to collect user information, serve ads, and more.

Cookies have two major disadvantages:

  • Each request needs to be transmitted, so it cannot be used to store large amounts of data;
  • Low security, through the browser tools, it is easy to see the cookie set by the website server.

Gorilla /securecookie provides a securecookie by encrypting the cookie on the server, making its contents unreadable and unforgery. Of course, sensitive information is strongly discouraged from being placed in cookies.

Quick to use

The code in this article uses Go Modules.

Create directory and initialize:

$ mkdir gorilla/securecookie && cd gorilla/securecookie
$ go mod init github.com/darjun/go-daily-lib/gorilla/securecookie

To install the Gorilla/Securecookie library:

$ go get github.com/gorilla/securecookie
package main

import (
  "fmt"
  "github.com/gorilla/mux"
  "github.com/gorilla/securecookie"
  "log"
  "net/http"
)

type User struct {
  Name string
  Age int
}

var (
  hashKey = securecookie.GenerateRandomKey(16)
  blockKey = securecookie.GenerateRandomKey(16)
  s = securecookie.New(hashKey, blockKey)
)

func SetCookieHandler(w http.ResponseWriter, r *http.Request) {
  u := &User {
    Name: "dj",
    Age: 18,
  }
  if encoded, err := s.Encode("user", u); err == nil {
    cookie := &http.Cookie{
      Name: "user",
      Value: encoded,
      Path: "/",
      Secure: true,
      HttpOnly: true,
    }
    http.SetCookie(w, cookie)
  }
  fmt.Fprintln(w, "Hello World")
}

func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
  if cookie, err := r.Cookie("user"); err == nil {
    u := &User{}
    if err = s.Decode("user", cookie.Value, u); err == nil {
      fmt.Fprintf(w, "name:%s age:%d", u.Name, u.Age)
    }
  }
}

func main() {
  r := mux.NewRouter()
  r.HandleFunc("/set_cookie", SetCookieHandler)
  r.HandleFunc("/read_cookie", ReadCookieHandler)
  http.Handle("/", r)
  log.Fatal(http.ListenAndServe(":8080", nil))
}

First you need to create a SecureCookie object:

var s = securecookie.New(hashKey, blockKey)

HashKey is mandatory and is used to verify whether the cookie is forged. The underlying hash-based Message Authentication code (HMAC) algorithm is used. 32/64 byte keys are recommended for hashkeys.

The blockKey is optional and is used to encrypt cookies. If encryption is not required, you can pass nil. If yes, its length must be the same as the block size of the corresponding encryption algorithm. For example, the block size of AES-128, AES-192, and AES-256 is 16/24/32 bytes.

For convenience, you can also use the GenerateRandomKey() function to generate a random key with sufficient security. The function returns a different key each time it is called. This is how the above code creates the key.

Calling s.encode (“user”, u) to encode the object U as a string actually uses the standard library encoding/gob internally. So any type that goB supports can be encoded.

Call s.decode (“user”, cookie.value, u) to decode the cookie Value into the corresponding U object.

Run:

$ go run main.go

Localhost :8080/set_cookie (localhost:8080/set_cookie)

Localhost :8080/read_cookie: DJ age: 18

Using JSON

Securecookie encodes cookie values using Encoding /gob by default, or we can use encoding/ JSON instead. Securecookie encapsulates the codec into a Serializer interface:

type Serializer interface {
  Serialize(src interface{}) ([]byte, error)
  Deserialize(src []byte, dst interface{}) error
}

Securecookie provides GobEncoder and JSONEncoder implementations:

func (e GobEncoder) Serialize(src interface{}) ([]byte, error) { buf := new(bytes.Buffer) enc := gob.NewEncoder(buf) if err := enc.Encode(src); err ! = nil { return nil, cookieError{cause: err, typ: usageError} } return buf.Bytes(), nil } func (e GobEncoder) Deserialize(src []byte, dst interface{}) error { dec := gob.NewDecoder(bytes.NewBuffer(src)) if err := dec.Decode(dst); err ! = nil { return cookieError{cause: err, typ: decodeError} } return nil } func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) { buf := new(bytes.Buffer) enc := json.NewEncoder(buf) if err := enc.Encode(src); err ! = nil { return nil, cookieError{cause: err, typ: usageError} } return buf.Bytes(), nil } func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error { dec := json.NewDecoder(bytes.NewReader(src)) if err := dec.Decode(dst); err ! = nil { return cookieError{cause: err, typ: decodeError} } return nil }

We can call securecookie. SetSerializer (JSONEncoder {}) set using JSON code:

var (
  hashKey = securecookie.GenerateRandomKey(16)
  blockKey = securecookie.GenerateRandomKey(16)
  s = securecookie.New(hashKey, blockKey)
)

func init() {
  s.SetSerializer(securecookie.JSONEncoder{})
}

Custom codec

We can define a type that implements the Serializer interface so that objects of that type can be used as codecs for securecookie. We implement a simple XML codec:

package main type XMLEncoder struct{} func (x XMLEncoder) Serialize(src interface{}) ([]byte, error) { buf := &bytes.Buffer{} encoder := xml.NewEncoder(buf) if err := encoder.Encode(buf); err ! = nil { return nil, err } return buf.Bytes(), nil } func (x XMLEncoder) Deserialize(src []byte, dst interface{}) error { dec := xml.NewDecoder(bytes.NewBuffer(src)) if err := dec.Decode(dst); err ! = nil { return err } return nil } func init() { s.SetSerializer(XMLEncoder{}) }

Because securecookie. CookieError is not exported, XMLEncoder and GobEncoder/JSONEncoder return a somewhat different error, but it does not affect usage.

Hash/Block function

Securecookie uses sha256.New as the Hash function (for the HMAC algorithm) and AES. NewCipher as the Block function (for encryption and decryption) by default:

// securecookie.go func New(hashKey, blockKey []byte) *SecureCookie { s := &SecureCookie{ hashKey: hashKey, blockKey: BlockKey, // here set Hash function hashFunc: sha256.New, maxAge: 86400 * 30, maxLength: 4096, sz: GobEncoder{}, } if hashKey == nil { s.err = errHashKeyNotSet } if blockKey ! = nil {// here set Block function s.blockfunc (aes.newcipher)} return s}

You can modify the Hash function via securecookie.hashFunc () by passing in a func () Hash type:

func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie {
  s.hashFunc = f
  return s
}

Modify the Block function via securecookie.blockfunc (), passing an f func([]byte) (cipher.Block, error) :

func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie {
  if s.blockKey == nil {
    s.err = errBlockKeyNotSet
  } else if block, err := f(s.blockKey); err == nil {
    s.block = block
  } else {
    s.err = cookieError{cause: err, typ: usageError}
  }
  return s
}

Replacing these two functions is more for security reasons, such as choosing a more secure SHA512 algorithm:

s.HashFunc(sha512.New512_256)

Change the Key

To prevent cookie leakage from causing security risks, there is a common security policy: periodically change the Key. Change the Key to invalidate the cookie. The corresponding Securecookie library is to replace the Securecookie object:

var (
  prevCookie    unsafe.Pointer
  currentCookie unsafe.Pointer
)

func init() {
  prevCookie = unsafe.Pointer(securecookie.New(
    securecookie.GenerateRandomKey(64),
    securecookie.GenerateRandomKey(32),
  ))
  currentCookie = unsafe.Pointer(securecookie.New(
    securecookie.GenerateRandomKey(64),
    securecookie.GenerateRandomKey(32),
  ))
}

When the program starts, we create two SecureCookie objects and generate a new object to replace the old one every once in a while.

Since each request is processed in a separate goroutine (read), the replacement key is also handled in a separate goroutine (write). For concurrency security, we must add synchronization measures. But it’s too heavy to use a lock in this case, because the frequency of updates is so low. I here will be securecookie securecookie object stored as unsafe. The Pointer type, and then you can use the atomic atomic operations to synchronous read and update:

func rotateKey() {
  newcookie := securecookie.New(
    securecookie.GenerateRandomKey(64),
    securecookie.GenerateRandomKey(32),
  )

  atomic.StorePointer(&prevCookie, currentCookie)
  atomic.StorePointer(&currentCookie, unsafe.Pointer(newcookie))
}

RotateKey () needs to be called periodically in a new goroutine, which we start in the main function

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  defer cancel()
  go RotateKey(ctx)
}

func RotateKey(ctx context.Context) {
  ticker := time.NewTicker(30 * time.Second)
  defer ticker.Stop()

  for {
    select {
    case <-ctx.Done():
      break
    case <-ticker.C:
    }

    rotateKey()
  }
}

For testing purposes, I set it to rotate every 30 seconds. And to prevent goroutine leaks, we pass in a Context that can be cancelled. Note also that the * time.ticker object created by time.newticker () needs to be closed manually by calling Stop() when not in use, otherwise resource leaks will occur.

After using two SecureCookie objects, our codec can call the EncodeMulti/DecodeMulti set of methods that can accept multiple SecureCookie objects:

func SetCookieHandler(w http.ResponseWriter, r *http.Request) { u := &User{ Name: "dj", Age: 18, } if encoded, err := securecookie.EncodeMulti( "user", u, / / here ๐Ÿ’ (* securecookie securecookie) (atomic) LoadPointer (& currentCookie)),); err == nil { cookie := &http.Cookie{ Name: "user", Value: encoded, Path: "/", Secure: true, HttpOnly: true, } http.SetCookie(w, cookie) } fmt.Fprintln(w, "Hello World") }

After the SecureCookie object is saved using unsafe.Pointer, type conversion is required. And because of concurrency issues, you need to use atomic.loadPointer () access.

CurrentCookie and prevCookie are passed to DecodeMulti, so the prevCookie does not expire immediately:

func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie("user"); err == nil { u := &User{} if err = securecookie.DecodeMulti( "user", cookie.Value, u, / / here ๐Ÿ’ (* securecookie securecookie) (atomic) LoadPointer (& currentCookie)), (*securecookie.SecureCookie)(atomic.LoadPointer(&prevCookie)), ); err == nil { fmt.Fprintf(w, "name:%s age:%d", u.Name, u.Age) } else { fmt.Fprintf(w, "read cookie error:%v", err) } } }

Run the program:

$ go run main.go

Request localhost:8080/set_cookie, then request localhost:8080/read_cookie to read the cookie. Wait 1 minute, request again, find the previous cookie invalid:

read cookie error:securecookie: the value is not valid (and 1 other error)

conclusion

Securecookie adds a layer of protection to cookies so they cannot be easily read and forged. Again, it needs to be emphasized:

Don’t put sensitive data in cookies! Don’t put sensitive data in cookies! Don’t put sensitive data in cookies! Don’t put sensitive data in cookies!

Say important things four times. There are careful trade-offs when using cookies to store data.

If you find a fun and useful Go library, please Go to GitHub and submit the issue๐Ÿ˜„

reference

  1. Gorilla/securecookie GitHub:github.com/gorilla/securecookie
  2. Go a library GitHub:https://github.com/darjun/go-daily-lib daily

I

My blog: https://darjun.github.io

Welcome to follow my wechat public account [GoUpUp], learn together, make progress together ~