aboutsummaryrefslogblamecommitdiff
path: root/securecookie.go
blob: a66b044096bd759a652927e32d997be40f4bc646 (plain) (tree)
1
2
3
4
5
6
7
8







                                                           


                       
                       

                         

              

                 




                                                  

 
       

                      

 





                                                                 






















                                                                                               

                                  










                                                                            


                                                                               


                                                                             






                                                              

                                                                              


                 
                           
                               

                                      
                                        
         

                     




                                                                          
                        

                             


                       
                            
                          




                                                                                 







                                                                               
                                                         






                                                                           




                                                                                




































                                                                                      
                                         


                                                           
                                                                



                
                                                                    
  

                                                                             





                                                                   

                                 

                                                                                


                                                                              




                                                                              
                                                                               





















                                                                                         
         










                                                                           
         




                                                    


                        
                                                       
                                                                   



                                                             
                                                                           











                                                                            
                                                 














                                                                              
                            

                             
                                        
                            


                                                         
                                              



                                       
                          



                                                    
                                    



                                                                   
                          



                                                                             
                                          


                                              
                                         

                                              
                                          



                                 
                          


                                                             
                                  

                 
                          
                                                       
                                                                


                  























                                                                               


                                                                                  

                          
                            

 

                                                                               
                                                  



                                                                               

                                                 
 
                                

 



                                                                           







                                                                   

                                                                               

                                                                         
  

                                                                           
                                                                             

                                                                             
                                                             


                                         
         
 


                                            
         
 



                                                                 
         
 

                                                                      

 





                                                                        
         
 

                                                    

         




                                                                           

         
                         

 

                                                                               
                                                





                                                                          
                                                 



                                                                          
                                                                                                  





                                                                               








                                                               

 







                                                                             

                                                             






















                                                                                 















                                                                       

                                     




                                                                                   
                             
                                      

                                                         

                                           
                                            
         
                         





                                                                       

                                     




                                                                                     
                             
                                      

                                                     

                                  
                                            
         

                     
// Copyright 2012 The Gorilla Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package securecookie

import (
	"bytes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha512"
	"crypto/subtle"
	"encoding/base64"
	"fmt"
	"hash"
	"strconv"
	"time"

	"golang.org/x/crypto/nacl/secretbox"

	scrypt "github.com/elithrar/simple-scrypt"
	"github.com/pkg/errors"
)

const (
	keySize   = 32
	nonceSize = 12
)

// Codec defines an interface to encode and decode cookie values.
type Codec interface {
	Encode(name string, value interface{}) (string, error)
	Decode(name, value string, dst interface{}) error
}

// Options represents optional configuration that can be passed to a new SecureCookie instance.
type Options struct {
	// RotatedKeys is a list of signing/encryption keys to attempt if the primary
	// key fails. This is useful when you wish to update the key used for
	// signing/encrypting, without immediately breaking old sessions.
	// TODO(matt): Determine whether to have a key <-> NotAfter relation?
	RotatedKeys [][32]byte
	// EncryptCookies determines whether to encrypt cookie contents. This is
	// 'false' by default: cookies are signed to prevent tampering or manipulation
	// of values, but are not encrypted. Encryption adds size overhead to the
	// cookie contents, and it should be rare than an application is storing
	// sensitive data in a cookie. If you are, use TLS (HTTPS) as the transport
	// mechanism.
	EncryptCookies bool
	// MaxAge is the maximum age of a cookie, in seconds.
	MaxAge int64
	// Serialize determines how a cookie will be serialized. Defaults to
	// encoding/gob for compatibility with all Go types. An encoding/json based
	// serializer is also provided as a built-in option for improved performance
	// and reduced overhead (in bytes).
	Serialize Serializer
}

// New returns a new SecureCookie.
//
// TODO(matt): Update this - HMAC (signed, to prevent an attacker from
// modifying values). Generate keys outside of application & persist them
// securely. securecookie can generate keys for you, but failing to persist
// (store) them means that cookies cannot be verified (or de-crypted) if the
// application is restarted.
//
// The provided key must be 32-bytes (256 bits) in length.
//
// The same key is used for both signing-only and encrypted modes, as the
// encrypted mode used an AEAD construct.
//
// hashKey is required, used to authenticate values using HMAC. Create it using
// GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes.
//
// Note that keys created using GenerateRandomKey() are not automatically
// persisted. New keys will be created when the application is restarted, and
// previously issued cookies will not be able to be decoded.
func New(key [32]byte, opts *Options) (*SecureCookie, error) {
	if len(key) != keySize {
		return nil, errInvalidKey
	}

	if opts.RotatedKeys != nil {
		for idx, v := range opts.RotatedKeys {
			// TODO(matt): If there are keys, set tryKeys = true?
			// Rotated keys are only used for decryption attempts.
		}
	}

	s := &SecureCookie{
		key:       key,
		maxAge:    86400 * 30,
		maxLength: 4096,
		sz:        GobEncoder{},
	}

	return s, nil
}

// SecureCookie encodes and decodes authenticated and optionally encrypted
// cookie values.
type SecureCookie struct {
	key       []byte
	stretched bool
	encrypter cipher.AEAD
	maxLength int
	maxAge    int64
	minAge    int64
	sz        Serializer
	opts      *Options
	// For testing purposes, the function that returns the current timestamp.
	// If not set, it will use time.Now().UTC().Unix().
	timeFunc func() int64
}

// Serializer provides an interface for providing custom serializers for cookie
// values.
type Serializer interface {
	Serialize(src interface{}) ([]byte, error)
	Deserialize(src []byte, dst interface{}) error
}

// GobEncoder encodes cookie values using encoding/gob. This is the simplest
// encoder and can handle complex types via gob.Register.
type GobEncoder struct{}

// JSONEncoder encodes cookie values using encoding/json. Users who wish to
// encode complex types need to satisfy the json.Marshaller and
// json.Unmarshaller interfaces.
type JSONEncoder struct{}

// NopEncoder does not encode cookie values, and instead simply accepts a []byte
// (as an interface{}) and returns a []byte. This is particularly useful when
// you encoding an object upstream and do not wish to re-encode it.
type NopEncoder struct{}

// MaxLength restricts the maximum length, in bytes, for the cookie value.
//
// Default is 4096, which is the maximum value accepted by Internet Explorer.
func (s *SecureCookie) MaxLength(value int) *SecureCookie {
	s.maxLength = value
	return s
}

// MaxAge restricts the maximum age, in seconds, for the cookie value.
//
// Default is 86400 * 30. Set it to 0 for no restriction.
func (s *SecureCookie) MaxAge(value int) *SecureCookie {
	s.maxAge = int64(value)
	return s
}

// MinAge restricts the minimum age, in seconds, for the cookie value.
//
// Default is 0 (no restriction).
func (s *SecureCookie) MinAge(value int) *SecureCookie {
	s.minAge = int64(value)
	return s
}

// HashFunc sets the hash function used to create HMAC.
//
// Default is crypto/sha256.New.
func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie {
	s.hashFunc = f
	return s
}

// BlockFunc sets the encryption function used to create a cipher.Block.
//
// Default is crypto/aes.New.
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
}

// SetSerializer sets the encoding/serialization method for cookies.
//
// Default is encoding/gob.  To encode special structures using encoding/gob,
// they must be registered first using gob.Register().
func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie {
	s.sz = sz

	return s
}

// Encode encodes a cookie value.
//
// It serializes, optionally encrypts, signs with a message authentication code,
// and finally encodes the value.
//
// The name argument is the cookie name. It is stored with the encoded value.
// The value argument is the value to be encoded. It can be any value that can
// be encoded using the currently selected serializer; see SetSerializer().
//
// It is the client's responsibility to ensure that value, when encoded using
// the current serialization/encryption settings on s and then base64-encoded,
// is shorter than the maximum permissible length.
func (s *SecureCookie) Encode(name string, value interface{}) (string, error) {
	// 1. Check that the key exists
	// 2. Check whether we have KDF'ed the key yet (once only)
	// 3. Serialize our payload
	// 4. Check whether s.encrypt == true
	// 5. Generate our payload: name.expiry.data
	// 6. sign or encrypt
	// 7. encode to base64 URL safe
	// 8. Check that the maximum length does not exceed s.maxLenght (4096 by default)
	// 9. Return string(encoded)able

	var encoded string

	if s.key == nil {
		return encoded, errHashKeyNotSet
	}

	// Run the provided key through a KDF (once only).
	if !s.stretched {
		var err error
		if s.key, err = stretchKey(s.key); err != nil {
			return "", errInvalidKey
		}
	}

	var payload []byte
	// TODO(matt): create a helper here: generatePayload?
	if s.opts.EncryptCookies {
		// Encrypt and early return
		payload = []byte(fmt.Sprintf("%s|%d", name, s.timestamp()))
		if payload, err = s.encrypt(payload); err != nil {
			return "", errEncryptionFailed
		}
	} else {
		// HMAC -> return
	}

	// base64.URLEncoding.EncodeToString
	// Check length - len(encoded) > s.maxLength
	// return encoded, nil

	var err error
	var b []byte
	// 1. Serialize.
	if b, err = s.sz.Serialize(value); err != nil {
		return "", cookieError{cause: err, typ: usageError}
	}
	// 2. Encrypt (optional).
	if s.block != nil {
		if b, err = encrypt(s.block, b); err != nil {
			return "", cookieError{cause: err, typ: usageError}
		}
	}
	b = encode(b)
	// 3. Create MAC for "name|date|value". Extra pipe to be used later.
	b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b))
	mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1])
	// Append mac, remove name.
	b = append(b, mac...)[len(name)+1:]
	// 4. Encode to base64.
	b = encode(b)
	// 5. Check length.
	if s.maxLength != 0 && len(b) > s.maxLength {
		return "", errEncodedValueTooLong
	}
	// Done.
	return string(b), nil
}

// Decode decodes a cookie value.
//
// It decodes, verifies a message authentication code, optionally decrypts and
// finally deserializes the value.
//
// The name argument is the cookie name. It must be the same name used when
// it was stored. The value argument is the encoded cookie value. The dst
// argument is where the cookie will be decoded. It must be a pointer.
func (s *SecureCookie) Decode(name, value string, dst interface{}) error {
	if s.err != nil {
		return s.err
	}
	if s.hashKey == nil {
		s.err = errHashKeyNotSet
		return s.err
	}
	// 1. Check length.
	if s.maxLength != 0 && len(value) > s.maxLength {
		return errValueToDecodeTooLong
	}
	// 2. Decode from base64.
	b, err := decode([]byte(value))
	if err != nil {
		return err
	}
	// 3. Verify MAC. Value is "date|value|mac".
	parts := bytes.SplitN(b, []byte("|"), 3)
	if len(parts) != 3 {
		return ErrMacInvalid
	}
	h := hmac.New(s.hashFunc, s.hashKey)
	b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...)
	if err = verifyMac(h, b, parts[2]); err != nil {
		return err
	}
	// 4. Verify date ranges.
	var t1 int64
	if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil {
		return errTimestampInvalid
	}
	t2 := s.timestamp()
	if s.minAge != 0 && t1 > t2-s.minAge {
		return errTimestampTooNew
	}
	if s.maxAge != 0 && t1 < t2-s.maxAge {
		return errTimestampExpired
	}
	// 5. Decrypt (optional).
	b, err = decode(parts[1])
	if err != nil {
		return err
	}
	if s.block != nil {
		if b, err = decrypt(s.block, b); err != nil {
			return err
		}
	}
	// 6. Deserialize.
	if err = s.sz.Deserialize(b, dst); err != nil {
		return cookieError{cause: err, typ: decodeError}
	}
	// Done.
	return nil
}

// timestamp returns the current timestamp, in seconds.
//
// For testing purposes, the function that generates the timestamp can be
// overridden. If not set, it will return time.Now().UTC().Unix().
func (s *SecureCookie) timestamp() int64 {
	if s.timeFunc == nil {
		return time.Now().UTC().Unix()
	}
	return s.timeFunc()
}

// Authentication -------------------------------------------------------------

// createMac creates a message authentication code (MAC).
func createMac(h hash.Hash, value []byte) []byte {
	h.Write(value)
	return h.Sum(nil)
}

// verifyMac verifies that a message authentication code (MAC) is valid.
func verifyMac(h hash.Hash, value []byte, mac []byte) error {
	mac2 := createMac(h, value)
	// Check that both MACs are of equal length, as subtle.ConstantTimeCompare
	// does not do this prior to Go 1.4.
	if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 {
		return nil
	}
	return ErrMacInvalid
}

// Authentication -------------------------------------------------------------

// sign returns a signature for the provided data.
//
// Internally, sign uses HMAC-SHA-512/256, which is HMAC-SHA-512 truncated to a
// 256-bit output to prevent length-extension attacks.
func (s *SecureCookie) sign(data []byte) ([]byte, error) {
	mac := hmac.New(sha512.New512_256, s.key)
	mac.Write(data)

	return mac.Sum(nil), nil
}

// verify validates that the provided data matches the given signature.
//
// verify uses HMAC-SHA-512/256 for signatures and performs a constant-time
// comparison of signatures using Go's hmac.Equal function.
func (s *SecureCookie) verify(data []byte, actualMAC []byte) bool {
	mac := hmac.New(sha512.New512_256, s.key)
	mac.Write(data)
	expected := mac.Sum(nil)

	return hmac.Equal(expected, actualMAC)
}

// Encryption -----------------------------------------------------------------

// encrypt encrypts the provided data using nacl/secretbox, and returns a
// concatenation of nonce+ciphertext.
//
// Interally, encrypt uses XSalsa20+Poly1305 (an AEAD; combining a stream
// cipher & MAC construct) and generates a random, 192-bit nonce using Go's
// crypto/rand library, which leverages /dev/urandom or the equivalent on all
// platforms. A random nonce is used to prevent nonce re-use issues, and does
// not require the package or package user to increment nonces.
func (s *SecureCookie) encrypt(data []byte) ([]byte, error) {
	// 1. Check our key is not nil
	if s.key == nil {
		return nil, errInvalidKey
	}

	// 2. Check that our data is not nil
	if data == nil {
		return nil, errInvalidData
	}

	// 3. Generate a fresh 96 bit nonce
	nonce, err := GenerateRandomBytes(12)
	if err != nil {
		return nil, errors.Wrap(err, "encryption failed")
	}

	// 4. Encrypt our data, appending the ciphertext to the nonce.
	return secretbox.Seal(nonce[:], data, nonce, s.key), nil
}

// decrypt decrypts the provided nonce+ciphertext using nacl/secretbox.
//
// It expects that the the 196-bit nonce is prepended to the ciphertext.
func (s *SecureCookie) decrypt(encrypted []byte) ([]byte, error) {
	if s.key == nil {
		return nil, errInvalidKey
	}

	if encrypted == nil || len(encrypted) < 24 {
		return nil, errDecryptionFailed
	}

	// 3. Parse our nonce
	nonce := encrypted[24:]
	ptext, ok := secretbox.Open([]byte{}, encrypted[24:], nonce, s.key)
	if !ok {
		return nil, errDecryptionFailed
	}

	return ptext, nil
}

// Encoding -------------------------------------------------------------------

// encode encodes a value using URL-safe base64.
func encode(value []byte) []byte {
	encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value)))
	base64.URLEncoding.Encode(encoded, value)
	return encoded
}

// decode decodes a cookie using URL-safe base64.
func decode(value []byte) ([]byte, error) {
	decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value)))
	b, err := base64.URLEncoding.Decode(decoded, value)
	if err != nil {
		return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"}
	}
	return decoded[:b], nil
}

// Helpers --------------------------------------------------------------------

// GenerateRandomBytes returns securely generated random bytes.
// It will return an error if the system's secure random
// number generator fails to function correctly, in which
// case the caller should not continue.
func GenerateRandomBytes(n int) ([]byte, error) {
	b := make([]byte, n)
	_, err := rand.Read(b)
	// Note that err == nil only if we read len(b) bytes.
	return b, err
}

// stretchKey passes the provided authentication/encryption key through a KDF
// (scrypt) to improve the entropy of the key used
func stretchKey(key []byte) ([]byte, error) {
	return scrypt.GenerateFromPassword(key, scrypt.DefaultParams)
}

// TODO(matt): RotatedEncrypt / RotatedDecrypt?

// CodecsFromPairs returns a slice of SecureCookie instances.
//
// It is a convenience function to create a list of codecs for key rotation. Note
// that the generated Codecs will have the default options applied: callers
// should iterate over each Codec and type-assert the underlying *SecureCookie to
// change these.
//
// Example:
//
//      codecs := securecookie.CodecsFromPairs(
//           []byte("new-hash-key"),
//           []byte("new-block-key"),
//           []byte("old-hash-key"),
//           []byte("old-block-key"),
//       )
//
//      // Modify each instance.
//      for _, s := range codecs {
//             if cookie, ok := s.(*securecookie.SecureCookie); ok {
//                 cookie.MaxAge(86400 * 7)
//                 cookie.SetSerializer(securecookie.JSONEncoder{})
//                 cookie.HashFunc(sha512.New512_256)
//             }
//         }
//
func CodecsFromPairs(keyPairs ...[]byte) []Codec {
	codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2)
	for i := 0; i < len(keyPairs); i += 2 {
		var blockKey []byte
		if i+1 < len(keyPairs) {
			blockKey = keyPairs[i+1]
		}
		codecs[i/2] = New(keyPairs[i], blockKey)
	}
	return codecs
}

// EncodeMulti encodes a cookie value using a group of codecs.
//
// The codecs are tried in order. Multiple codecs are accepted to allow
// key rotation.
//
// On error, may return a MultiError.
func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) {
	if len(codecs) == 0 {
		return "", errNoCodecs
	}

	var errors MultiError
	for _, codec := range codecs {
		encoded, err := codec.Encode(name, value)
		if err == nil {
			return encoded, nil
		}
		errors = append(errors, err)
	}
	return "", errors
}

// DecodeMulti decodes a cookie value using a group of codecs.
//
// The codecs are tried in order. Multiple codecs are accepted to allow
// key rotation.
//
// On error, may return a MultiError.
func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error {
	if len(codecs) == 0 {
		return errNoCodecs
	}

	var errors MultiError
	for _, codec := range codecs {
		err := codec.Decode(name, value, dst)
		if err == nil {
			return nil
		}
		errors = append(errors, err)
	}
	return errors
}