securecookie

package module
v1.3.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 18, 2023 License: MIT Imports: 12 Imported by: 3

README

GoDoc Build Coverage Go Report Status release

Encode and Decode secure cookies

This package provides functions to encode and decode secure cookie values.

A secure cookie has its value ciphered and signed with a message authentication code. This prevents the remote cookie owner from knowing what information is stored in the cookie or modifying it. It also prevents an attacker from forging a fake cookie.

This package differs from the Gorilla secure cookie in that its encoding and decoding is 3 times faster and needs no heap allocation with an equivalent security strength. Both use AES128 and SHA256 to secure the value.

Note: This package uses its own secure cookie value encoding. It is thus incompatible with the Gorilla secure cookie package and the ones provided with other language frameworks. This encoding is simpler and more efficient, and adds a version number to support evolution with backwards compatibility.

Warning: Because this package impacts security of web applications, it is a critical functionality. Review feedbacks are always welcome.

If you enjoy the code and want to thank me 🙏, you can buy me a coffee.

Content

Installation

To install or update this secure cookie package use the instruction:

go get -u "github.com/chmike/securecookie"

Usage examples

To use this cookie package in your server, add the following import.

import "github.com/chmike/securecookie"
Generating a random key

It is strongly recommended to generate the random key with the following function. Save the key in a file using hex.EncodeToString() and restrict access to that file.

var key []byte = securecookie.MustGenerateRandomKey()

To mitigate the risk of an attacker getting the saved key, you might store a second key in another place and use the xor of both keys as secure cookie key. The attacker will have to get both keys to reconstruct the effective key which should be more difficult.

A secure cookie is instantiated with the New function. It returns an error if an argument is invalid.

obj, err := securecookie.New("session", key, securecookie.Params{
		Path:     "/sec",           // cookie received only when URL starts with this path
		Domain:   "example.com",    // cookie received only when URL domain matches this one
		MaxAge:   3600,             // cookie becomes invalid 3600 seconds after it is set
		HTTPOnly: true,             // disallow access by remote javascript code 
		Secure:   true,             // cookie received only with HTTPS, never with HTTP
		SameSite: securecookie.Lax, // cookie received with same or sub-domain names
})
if err != nil {
    // ...
}

It is also possible to instantiate a secure cookie object without returning an error and panic if an argument is invalid. To do this, use securecookie.MustNew(). In the following example, session is the cookie name and the Path is /sec. A secured value may be stored in the remote browser by calling the SetValue() method. After that, every subsequent request from that browser with a URL starting with /sec will have the cookie sent along. Calling the method GetValue() will extract the secure value from the request. A request to delete the cookie may be sent to the remote browser by calling the method Delete().

var obj = securecookie.MustNew("Auth", key, securecookie.Params{
		Path:     "/sec",           // cookie received only when URL starts with this path
		Domain:   "example.com",    // cookie received only when URL domain matches this one
		MaxAge:   3600,             // cookie becomes invalid 3600 seconds after it is set
		HTTPOnly: true,             // disallow access by remote javascript code 
		Secure:   true,             // cookie received only with HTTPS, never with HTTP
		SameSite: securecookie.Lax, // cookie received with same or sub-domain names
}

Remember that the key should not be stored in the source code or in a repository.

var val = []byte("some value")
// with w as the http.ResponseWriter
if err := obj.SetValue(w, val); err != nil {
    // ...
}

The value is appended to the given buffer. If buf is nil a new buffer ([]byte) is allocated. If buf is too small it is grown.

// with r as the *http.Request
val, err := obj.GetValue(buf, r) 
if err != nil {
  // ...
}

The returned value is of type []byte.

// with w as the http.ResponseWriter
if err := obj.Delete(w); err != nil {
  // ...
}

Note: don't rely on the assumption that the remote user agent (browser) will effectively delete the cookie. Evil users will try anything to break your site.

Complete example

An complete example is provided here.

Follow these steps to test the example:

  1. create a directory in /tmp and set it as the working directory: "mkdir /tmp/sctest; cd /tmp/sctest"
  2. create a file named main.go and copy the example code referenced above into it
  3. create a go.mod file: "go mod init example.com"
  4. get the latest version of the securecookie packaqe: "go get github.com/chmike/securecookie@v1.2.0"
  5. run the server: "go run main.go"
  6. with your browser, request "http://localhost:8080/set/someValue" to set the secure cookie value
  7. with your browser, request "http://localhost:8080/val" to retriev the secure cookie value

Benchmarking

Encoding the cookie named "test" with value "some value". See benchmark functions are at the bottom of cookie_test.go file. The ns/op values were obtained by running the benchmark 10 times and taking the minimal value. These values were obtained with go1.14 (27-Feb-2020) on an Ubuntu OS with an i5-7400 3GHz processor.

Chmike Gorilla
Value len 84 112
Set ns/op 2393 6309
Get ns/op 1515 5199
Set B/op 350 2994
Get B/op 192 2720
Set allocs/op 3 35
Get allocs/op 2 38

The secure cookie value encoding and decoding functions of this package need 0 heap allocations.

The benchmarks were obtained with release v0.4. Subsequent release may alter the benchmark results.

Qualitative comparison

The latest version was updated to put the security in line with the Gorilla secure cookie.

  • We both use CTR-AES-128 encryption with a 16 byte nonce, and HMAC-SHA-256.
  • We both encrypt first then compute the MAC over the cipher text.
  • A time stamp is added to the encoded value.
  • The hmac is computed over the cookie value name, the ciphered time stamp and value.
  • Both packages don't take special measures to secure the secret key.
  • Both packages don't effectively conceal the value byte length.

The differences between the Gorilla secure cookie and this implementation are:

  • This code is more efficient, and there is still room for improvement.
  • This secure value encoding is more compact without weakening the security.
  • This secure cookie encoding is incompatible with other secure cookie encoding. I don't know the status of Gorilla's encoding.
  • This encoding adds an encoding version number allowing to change or add new encoding without breaking backwards compatibility. Gorilla doesn't have this.
  • This package provides a Delete cookie method.

This package and Gorilla both provide equivalently secure cookies if we discard the fact that no special measure is taken to conceal the key in memory and the value length. This package is quite new and needs more reviews to validate the security of the implementation.

Feedback and contributions are welcome.

Value encoding

  1. A clear text message is first assembled as follow:

    [tag][nonce][stamp][value][padding]

  • The tag is 1 byte. The 6 most significant bits encode the version number of the encoding (currently 0). The 2 less significant bits encode the number of padding bytes (0, 1 or 2). 3 is an invalid padding length. The number is picked so that the total length including the MAC is a multiple of 3. This simplifies base64 encoding by avoiding its padding.
  • The nonce is 16 bytes long (AES block length) and contains cryptographically secure pseudorandom bytes.
  • The stamp is the unix time subtracted by an epochOffset value (1505230500), and encoded using the LEB128 encoding.
  • The value is a copy of the user provided value to secure.
  • The padding bytes are cryptographically secure pseudorandom bytes. There may be 0 to 2 padding bytes. The number is picked so that the total length including the MAC is a multiple of 3. This simplifies base64 encoding by avoiding its padding.
  1. The stamp, value and padding bytes are ciphered using CTR-AES with the 16 last bytes of the key as ciphering key. The nonce is used as iv and counter initialization value. The tag and nonce are left unciphered.

  2. An HMAC-SHA-256 is computed over (1) the cookie name and (2) the bytes sequence obtained after step 2. The 32 byte long MAC is appended after the padding.

  3. The whole byte sequence, from the tag to the last byte of the MAC is encoded in Base64 using the URL encoding. There is no padding since the byte length is a multiple of 3 bytes.

The tag which provides an encoding version allows completely changing the encoding while preserving backwards compatibility if required.

Contributors

  • lstokeworth (reddit):
    • suggest to replace copy with append,
    • remove the Expires Params field and use only the MaxAge,
    • provide a constant date in the past for Delete.
  • cstockton (github):
  • flowonyx (github):
    • fix many typos in the README and comments.

Usage advise

It is very important to understand that the security is limited to the cookie content. Nothing proves that the other data received by the server with a secure cookie has been created by the user's request.

When you have properly set the domain path, and HTTPOnly with the Secure flag, and use HTTPS, only the user's browser can send the cookie. But it is still possible for an attacker to trick your browser to send a request to the site without the user's knowledge and consent. This is known as a CSRF attack.

Consider this scenario with a Pizza ordering web site. First let's see what happens when everything goes as expected.

The user has to login to the site to be allowed to order pizzas. During the login transaction a secure cookie is added into the user's browser. The user is then shown a form with the number of pizzas to order. When the user clicks the Order button, his browser will make a request to an URL provided with the form. It will join the field values and the secure cookie since the URL path and domain match the one specified at the login transaction.

When the server receives this request, it checks the cookie validity to determine who that client is and if he is legitimate. All is fine. The order is then forwarded to the pizza chef. The pizza is later delivered to the user.

Now comes the villain. He sets up some random site with a form and a validation button that the victim will very likely click (e.g., "Subscribe to spam" with a Please no button as validation button). The villain has set up the form so that the URL associated to the validation button is the URL to order pizzas. He will have added a hidden field with the number of pizzas to order set to 10!

When the user clicks that validation button, his browser will send a request to the pizza ordering site with the field value and the secure cookie since the URL matches the cookie path and domain.

The pizza ordering site checks the secure cookie and it will be authenticated. It will assume that the user issued that order. When the delivery man rings at the user's door with 10 pizzas in his hand, there will be a conflict and no way to know to know who's fault it is. Notice how the value 10 associated with the cookie in the ordering request was not signed by the user's browser.

To avoid this, the solution is to add a way to authenticate the form response. This is done by adding a hidden field in the form with a random byte sequence, and set a secure cookie with that byte sequence as value and a validity date limit. When the user fills that form, the server will receive back the secure cookie and the hidden field value. The server then checks that they match to validate the response.

An attacker can forge a random byte sequence, but can't forge the secure cookie that goes with it.

Note that this protection is void in case of XSS attack (script injection).

The above method works with forms, not with REST API like requests because the server can't send the random token to the client that can be used as a challenge. For REST API like authenticated transactions, the client and server have to both know a secret byte sequence they use to compute a hmac value over the URI, the method, the data and a message sequence number. They can then authenticate the message and the source.

The secret byte sequence can be determined in the authentication transaction with public and private keys. There is no need for TLS to securely authenticate the client and server. A secret cookie is no help here.

Documentation

Overview

Package securecookie is a fast, efficient and safe cookie value encoder/decoder.

A secure cookie has its value ciphered and signed with a message authentication code. This prevents the remote cookie owner from knowing what information is stored in the cookie or modifying it. It also prevents an attacker from forging a fake securecookie.

What makes this secure cookie package different is that it is fast (faster than the Gorilla secure cookie), and the value encoding and decoding needs no heap allocations.

The intended use is to instantiate all secure cookie objects of your web site at program start up. In the following example "session" is the cookie name. The key is a byte sequence generated with the GenerateRandomKey() function. The same key must be used to retrieve the value.

obj, err := securecookie.New("session", key, securecookie.Params{
	Path:     "/sec",        // cookie received only when URL
	                         // starts with this path.
	Domain:   "example.com", // cookie received only when URL
	                         // domain matches this one.
	MaxAge:   3600,          // cookie becomes invalid 3600
	                         // seconds after it is set.
	HTTPOnly: true,          // disallow access by remote
	                         // javascript code.
	Secure:   true,          // cookie received only with HTTPS,
							 // never with HTTP.
    SameSite: securecookie.Strict, // cookie returned only
})
if err != nil {
	// ...
}

An securecookie can also be instantiated as a global variable with the MustNew function. The function will panic if an error occurs.

var obj = securecookie.MustNew("session", key, securecookie.Params{
	Path:     "/sec",
	Domain:   "example.com",
	MaxAge:   3600,
	HTTPOnly: true,
	Secure:   true,
})

A securecookie object is immutable. It's methods can be called from different goroutines. The Delete(), SetValue() and GetValue() methods are independent.

To set a cookie value, call the SetValue() method with a http.ResponseWriter w, and a value val as arguments.

    var val = []byte("some value")
    if err := obj.SetValue(w, val); err != nil {
		// ...
	}

A securecookie value is retrieved from an http.Request with the GetValue() method and is appended to the given buffer, extending it if required. If buf is nil, a new buffer is allocated.

    val, err := obj.GetValue(buf, r)
	if err != nil {
		// ...
	}

A securecookie is deleted by calling the Delete() method with the http.ResponseWriter w as argument.

    if err := obj.Delete(r); err != nil {
		// ...
	}

Index

Constants

View Source
const (
	Undefined int = iota
	Lax
	None
	Strict
)

Keywords used for parameter value.

View Source
const KeyLen = 32

KeyLen is the byte length of the key.

Variables

This section is empty.

Functions

func GenerateRandomKey

func GenerateRandomKey() ([]byte, error)

GenerateRandomKey returns a random key of KeyLen bytes. Use hex.EncodeToString(key) to get the key as hexadecimal string, and hex.DecodeString(keyStr) to convert back from string to byte slice.

func MustGenerateRandomKey added in v1.1.0

func MustGenerateRandomKey() []byte

MustGenerateRandomKey returns a random key of KeyLen bytes. It calls GenerateRandomKey and panics if it returns an error.

Types

type Obj

type Obj struct {
	// contains filtered or unexported fields
}

Obj is a validated cookie object.

func MustNew

func MustNew(name string, key []byte, p Params) *Obj

MustNew panics if New returns a non-nil error, otherwise returns a cookie object. The intended use is to instantiate the object as a global variable.

func New

func New(name string, key []byte, p Params) (*Obj, error)

New instantiates a validated cookie object with the given cookie name, key and parameters.

func (*Obj) Delete

func (o *Obj) Delete(w http.ResponseWriter) error

Delete adds a request to the delete the securecookie to the server response w. The user agent may ignore the request.

func (*Obj) Domain

func (o *Obj) Domain() string

Domain returns the securecookie domain field value.

func (*Obj) GetValue

func (o *Obj) GetValue(dst []byte, r *http.Request) ([]byte, error)

GetValue returns the value of the first cookie with a matching name. The function uses the dst slice as storage if not nil and big enough.

func (*Obj) GetValueAndStamp added in v1.3.3

func (o *Obj) GetValueAndStamp(dst []byte, r *http.Request) ([]byte, time.Time, error)

GetValueAndStamp appends the decoded securecookie value to dst. dst is allocated if nil, or grown if too small. Returns the value and the time stamp truncated to seconds when the cookie was generated and added to an http response with SetValue.

func (*Obj) GetValues added in v1.3.4

func (o *Obj) GetValues(cookies []*http.Cookie) []ValueResult

GetValues returns the values and stamp of cookies with a matching name in cookies, or nil if none. GetValues should be user in place of GetValue when a request may contain multiple cookies with a same name. For each returned value, Error is nil and Value not nil, or Error is not nil and value is nil.

func (*Obj) HTTPOnly

func (o *Obj) HTTPOnly() bool

HTTPOnly returns the securecookie HTTPOnly field value.

func (*Obj) MaxAge

func (o *Obj) MaxAge() int

MaxAge returns the securecookie max age field value.

func (*Obj) Name added in v1.2.0

func (o *Obj) Name() string

Name return the securecookie name field value.

func (*Obj) Path

func (o *Obj) Path() string

Path returns the securecookie path field value.

func (*Obj) SameSite added in v1.3.0

func (o *Obj) SameSite() int

SameSite returns the same site field value. Note that the value None is ignored when the cookie is not secure. When SameSite is undefined, the default is Lax.

func (*Obj) Secure

func (o *Obj) Secure() bool

Secure returns the securecookie HTTPOnly field value.

func (*Obj) SetValue

func (o *Obj) SetValue(w http.ResponseWriter, v []byte) error

SetValue adds the securecookie with the value v to the server response w. The value v is encrypted, signed with a MAC, and encoded in base64.

type Params

type Params struct {
	Path     string // Optional : URL path to which the cookie will be returned
	Domain   string // Optional : domain to which the cookie will be returned
	MaxAge   int    // Optional : time offset in seconds from now, must be >= 0
	HTTPOnly bool   // Optional : disallow access by remote javascript code
	Secure   bool   // Optional : cookie is only sent through HTTPS connections
	SameSite int    // Optional : (None, Lax, or Strict) restrict cookie return (None requires Secure)
}

Params holds the optional cookie parameters.

type ValueResult added in v1.3.4

type ValueResult struct {
	Value []byte
	Stamp time.Time
	Error error
}

ValueResult is a decoded value with its time stamp, or the error if any. ValueResult are returned by the GetValues method.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL