1

Introduction

A cookie is a mechanism used to transfer small amounts of data between a Web client (usually a browser) and a server. It is generated by the server and sent to the client for storage. The client will bring the cookie with each subsequent request. Cookies are now somewhat abused. Many companies use cookies to collect user information, place advertisements, and so on.

Cookies have two major disadvantages:

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

gorilla/securecookie provides a secure cookie. By encrypting the cookie on the server, its content is unreadable and cannot be forged. Of course, it is strongly recommended not to put sensitive information in cookies.

Quick to use

The code in this article uses Go Modules.

Create a directory and initialize:

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

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)

Among them, hashKey is required. It is used to verify whether the cookie is forged. The bottom layer uses the HMAC (Hash-based message authentication code) algorithm. It is recommended to use a 32/64 byte key for hashKey

blockKey is optional. It is used to encrypt cookies. If encryption is not required, you can pass nil . If set, its length must be consistent with the block size of the corresponding encryption algorithm. For example, for the AES series of algorithms, the corresponding block sizes of AES-128/AES-192/AES-256 are 16/24/32 bytes respectively.

For convenience, you can also use the GenerateRandomKey() function to generate a random key with sufficient security. Each time this function is called, a different key will be returned. The above code creates the key in this way.

Call s.Encode("user", u) the object u into a string, which actually uses the standard library encoding/gob . Therefore, all types supported by gob

Call s.Decode("user", cookie.Value, u) to decode the cookie value into the corresponding u object.

run:

$ go run main.go

First use a browser to visit localhost:8080/set_cookie , then you can see the content of the cookie in the Application tab of Chrome Developer Tools:

Visit localhost:8080/read_cookie , the page shows name: dj age: 18 .

Use JSON

securecookie uses encoding/gob encode the cookie value by 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 the implementation GobEncoder and JSONEncoder

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 the JSON encoding:

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 to implement the Serializer interface, then the object of this type can be used as the codec of 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 not exported, XMLEncoder and GobEncoder/JSONEncoder are somewhat inconsistent, but it does not affect the use.

Hash/Block function

securecookie uses sha256.New as the Hash function (for HMAC algorithm) by aes.NewCipher , and 0610339ff966ce as the Block function (for encryption and decryption):

// securecookie.go
func New(hashKey, blockKey []byte) *SecureCookie {
  s := &SecureCookie{
    hashKey:   hashKey,
    blockKey:  blockKey,
    // 这里设置 Hash 函数
    hashFunc:  sha256.New,
    maxAge:    86400 * 30,
    maxLength: 4096,
    sz:        GobEncoder{},
  }
  if hashKey == nil {
    s.err = errHashKeyNotSet
  }
  if blockKey != nil {
    // 这里设置 Block 函数
    s.BlockFunc(aes.NewCipher)
  }
  return s
}

You can modify the Hash function securecookie.HashFunc() func () hash.Hash type:

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

Modify the Block function through securecookie.BlockFunc() 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
}

The replacement of these two functions is more due to security considerations. For example, use the more secure sha512 algorithm:

s.HashFunc(sha512.New512_256)

Change Key

In order to prevent the disclosure of cookies from causing security risks, there is a commonly used security strategy: replace the Key regularly. Change the Key to invalidate the previously obtained cookie. Corresponding to the 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 first SecureCookie objects, and then generate a new object to replace the old one at regular intervals.

Since each request is processed in a separate goroutine (read), the key replacement is also performed in a separate goroutine (write). For concurrency safety, we must increase synchronization measures. But in this case, the use of locks is too heavy, after all, the frequency of updates here is very low. Here I store the securecookie.SecureCookie object as unsafe.Pointer , and then I can use the atomic atomic operation to read and update synchronously:

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 regularly in a new goroutine, we start this goroutine main

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 the convenience of testing, I set it to rotate every 30s. At the same time, in order to prevent goroutine from leaking, we passed in a cancelable Context . Also note time.NewTicker() created *time.Ticker objects need to manually invoke when not in use Stop() closed, otherwise it will cause resource leaks.

After using two SecureCookie objects, our codec can call EncodeMulti/DecodeMulti , they 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,
    // 看这里 🐒
    (*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 using unsafe.Pointer save the SecureCookie object, type conversion is required when using it. And due to concurrency issues, you need to use atomic.LoadPointer() access.

When decoding, call DecodeMulti pass in currentCookie and prevCookie , so that prevCookie will not be invalid 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,
      // 看这里 🐒
      (*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

First request localhost:8080/set_cookie , and then request localhost:8080/read_cookie read the cookie. After waiting for 1 minute, I requested again and found that the previous cookie was invalid:

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

Summarize

securecookie adds a protective cover to the cookie, so that the cookie cannot be easily read and forged. Still need to emphasize:

Do not put sensitive data in cookies! Do not put sensitive data in cookies! Do not put sensitive data in cookies! Do not put sensitive data in cookies!

The important thing is said 4 times. You need to weigh carefully when using cookies to store data.

If you find a fun and easy-to-use Go language library, welcome to submit an issue on the Go Daily Library GitHub😄

refer to

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

I

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

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


darjun
2.9k 声望358 粉丝