diff options
author | moraes <rodrigo.moraes@gmail.com> | 2012-10-03 08:53:51 +0400 |
---|---|---|
committer | moraes <rodrigo.moraes@gmail.com> | 2012-10-03 08:53:51 +0400 |
commit | f34e8230f6655498a5e469bd42f6cf2f2b87ad5d (patch) | |
tree | 62c8e99ab7cedd539849ee83d23c82514ac71d09 | |
parent | 18fe3372e264fec4432bd4477e78574501958194 (diff) |
Initial files.
-rw-r--r-- | LICENSE | 27 | ||||
-rw-r--r-- | doc.go | 61 | ||||
-rw-r--r-- | securecookie.go | 386 | ||||
-rw-r--r-- | securecookie_test.go | 152 |
4 files changed, 626 insertions, 0 deletions
@@ -0,0 +1,27 @@ +Copyright (c) 2012 Rodrigo Moraes. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. @@ -0,0 +1,61 @@ +// 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 gorilla/securecookie encodes and decodes authenticated and optionally +encrypted cookie values. + +Secure cookies can't be forged, because their values are validated using HMAC. +When encrypted, the content is also inaccessible to malicious eyes. + +To use it, first create a new SecureCookie instance: + + var hashKey = []byte("very-secret") + var blockKey = []byte("a-lot-secret") + var s = securecookie.New(hashKey, blockKey) + +The hashKey is required, used to authenticate the cookie value using HMAC. +It is recommended to use a key with 32 or 64 bytes. + +The blockKey is optional, used to encrypt the cookie value -- set it to nil +to not use encryption. If set, the length must correspond to the block size +of the encryption algorithm. For AES, used by default, valid lengths are +16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. + +Strong keys can be created using the convenience function GenerateRandomKey(). + +Once a SecureCookie instance is set, use it to encode a cookie value: + + func SetCookieHandler(w http.ResponseWriter, r *http.Request) { + value := map[string]string{ + "foo": "bar", + } + if encoded, err := s.Encode("cookie-name", value); err == nil { + cookie := &http.Cookie{ + Name: "cookie-name", + Value: encoded, + Path: "/", + } + http.SetCookie(w, cookie) + } + } + +Later, use the same SecureCookie instance to decode and validate a cookie +value: + + func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { + if cookie, err := r.Cookie("cookie-name"); err == nil { + value := make(map[string]string) + if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { + fmt.Fprintf(w, "The value of foo is %q", value["foo"]) + } + } + } + +We stored a map[string]string, but secure cookies can hold any value that +can be encoded using encoding/gob. To store custom types, they must be +registered first using gob.Register(). For basic types this is not needed; +it works out of the box. +*/ +package securecookie diff --git a/securecookie.go b/securecookie.go new file mode 100644 index 0000000..1c743fc --- /dev/null +++ b/securecookie.go @@ -0,0 +1,386 @@ +// 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/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/base64" + "encoding/gob" + "errors" + "fmt" + "hash" + "io" + "strconv" + "time" +) + +// 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 +} + +// New returns a new SecureCookie. +// +// 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. +// +// blockKey is optional, used to encrypt values. Create it using +// GenerateRandomKey(). The key length must correspond to the block size +// of the encryption algorithm. For AES, used by default, valid lengths are +// 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. +func New(hashKey, blockKey []byte) *SecureCookie { + s := &SecureCookie{ + hashKey: hashKey, + blockKey: blockKey, + hashFunc: sha256.New, + maxAge: 86400 * 30, + maxLength: 4096, + } + if hashKey == nil { + s.err = errors.New("securecookie: hash key is not set") + } + if blockKey != nil { + s.BlockFunc(aes.NewCipher) + } + return s +} + +// SecureCookie encodes and decodes authenticated and optionally encrypted +// cookie values. +type SecureCookie struct { + hashKey []byte + hashFunc func() hash.Hash + blockKey []byte + block cipher.Block + maxLength int + maxAge int64 + minAge int64 + err error + // For testing purposes, the function that returns the current timestamp. + // If not set, it will use time.Now().UTC().Unix(). + timeFunc func() int64 +} + +// 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 = errors.New("securecookie: block key is not set") + } else if block, err := f(s.blockKey); err == nil { + s.block = block + } else { + s.err = err + } + return s +} + +// Encode encodes 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 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 encoding/gob. To store special structures, they must be +// registered first using gob.Register(). +func (s *SecureCookie) Encode(name string, value interface{}) (string, error) { + if s.err != nil { + return "", s.err + } + if s.hashKey == nil { + s.err = errors.New("securecookie: hash key is not set") + return "", s.err + } + var err error + var b []byte + // 1. Serialize. + if b, err = serialize(value); err != nil { + return "", err + } + // 2. Encrypt (optional). + if s.block != nil { + if b, err = encrypt(s.block, b); err != nil { + return "", err + } + } + 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 "", errors.New("securecookie: the value is too long") + } + // 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 = errors.New("securecookie: hash key is not set") + return s.err + } + // 1. Check length. + if s.maxLength != 0 && len(value) > s.maxLength { + return errors.New("securecookie: the value is too long") + } + // 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 errors.New("securecookie: invalid value %v") + } + 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 errors.New("securecookie: invalid timestamp") + } + t2 := s.timestamp() + if s.minAge != 0 && t1 > t2-s.minAge { + return errors.New("securecookie: timestamp is too new") + } + if s.maxAge != 0 && t1 < t2-s.maxAge { + return errors.New("securecookie: expired timestamp") + } + // 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 = deserialize(b, dst); err != nil { + return err + } + // 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) + if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 { + return nil + } + return errors.New("securecookie: the value is not valid") +} + +// Encryption ----------------------------------------------------------------- + +// encrypt encrypts a value using the given block in counter mode. +// +// A random initialization vector (http://goo.gl/zF67k) with the length of the +// block size is prepended to the resulting ciphertext. +func encrypt(block cipher.Block, value []byte) ([]byte, error) { + iv := GenerateRandomKey(block.BlockSize()) + if iv == nil { + return nil, errors.New("securecookie: failed to generate random iv") + } + // Encrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + // Return iv + ciphertext. + return append(iv, value...), nil +} + +// decrypt decrypts a value using the given block in counter mode. +// +// The value to be decrypted must be prepended by a initialization vector +// (http://goo.gl/zF67k) with the length of the block size. +func decrypt(block cipher.Block, value []byte) ([]byte, error) { + size := block.BlockSize() + if len(value) > size { + // Extract iv. + iv := value[:size] + // Extract ciphertext. + value = value[size:] + // Decrypt it. + stream := cipher.NewCTR(block, iv) + stream.XORKeyStream(value, value) + return value, nil + } + return nil, errors.New("securecookie: the value could not be decrypted") +} + +// Serialization -------------------------------------------------------------- + +// serialize encodes a value using gob. +func serialize(src interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + if err := enc.Encode(src); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// deserialize decodes a value using gob. +func deserialize(src []byte, dst interface{}) error { + dec := gob.NewDecoder(bytes.NewBuffer(src)) + if err := dec.Decode(dst); err != nil { + return err + } + return nil +} + +// Encoding ------------------------------------------------------------------- + +// encode encodes a value using 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 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, err + } + return decoded[:b], nil +} + +// Helpers -------------------------------------------------------------------- + +// GenerateRandomKey creates a random key with the given strength. +func GenerateRandomKey(strength int) []byte { + k := make([]byte, strength) + if _, err := io.ReadFull(rand.Reader, k); err != nil { + return nil + } + return k +} + +// CodecsFromPairs returns a slice of SecureCookie instances. +// +// It is a convenience function to create a list of codecs for key rotation. +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. +func EncodeMulti(name string, value interface{}, + codecs ...Codec) (string, error) { + for _, codec := range codecs { + if encoded, err := codec.Encode(name, value); err == nil { + return encoded, nil + } + } + return "", errors.New("securecookie: the value could not be encoded") +} + +// DecodeMulti decodes a cookie value using a group of codecs. +// +// The codecs are tried in order. Multiple codecs are accepted to allow +// key rotation. +func DecodeMulti(name string, value string, dst interface{}, + codecs ...Codec) error { + for _, codec := range codecs { + if err := codec.Decode(name, value, dst); err == nil { + return nil + } + } + return errors.New("securecookie: the value could not be decoded") +} diff --git a/securecookie_test.go b/securecookie_test.go new file mode 100644 index 0000000..0187ba0 --- /dev/null +++ b/securecookie_test.go @@ -0,0 +1,152 @@ +// 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 ( + "crypto/aes" + "crypto/hmac" + "crypto/sha256" + "errors" + "fmt" + "testing" +) + +var testCookies = []interface{}{ + map[string]string{"foo": "bar"}, + map[string]string{"baz": "ding"}, +} + +var testStrings = []string{"foo", "bar", "baz"} + +func TestSecureCookie(t *testing.T) { + // TODO test too old / too new timestamps + compareMaps := func(m1, m2 map[string]interface{}) error { + if len(m1) != len(m2) { + return errors.New("different maps") + } + for k, v := range m1 { + if m2[k] != v { + return fmt.Errorf("Different value for key %v: expected %v, got %v", k, m2[k], v) + } + } + return nil + } + + s1 := New([]byte("12345"), []byte("1234567890123456")) + s2 := New([]byte("54321"), []byte("6543210987654321")) + value := map[string]interface{}{ + "foo": "bar", + "baz": 128, + } + + for i := 0; i < 50; i++ { + // Running this multiple times to check if any special character + // breaks encoding/decoding. + encoded, err1 := s1.Encode("sid", value) + if err1 != nil { + t.Error(err1) + continue + } + dst := make(map[string]interface{}) + err2 := s1.Decode("sid", encoded, &dst) + if err2 != nil { + t.Fatalf("%v: %v", err2, encoded) + } + if err := compareMaps(dst, value); err != nil { + t.Fatalf("Expected %v, got %v.", value, dst) + } + dst2 := make(map[string]interface{}) + err3 := s2.Decode("sid", encoded, &dst2) + if err3 == nil { + t.Fatalf("Expected failure decoding.") + } + } +} + +func TestAuthentication(t *testing.T) { + hash := hmac.New(sha256.New, []byte("secret-key")) + for _, value := range testStrings { + hash.Reset() + signed := createMac(hash, []byte(value)) + hash.Reset() + err := verifyMac(hash, []byte(value), signed) + if err != nil { + t.Error(err) + } + } +} + +func TestEncription(t *testing.T) { + block, err := aes.NewCipher([]byte("1234567890123456")) + if err != nil { + t.Fatalf("Block could not be created") + } + var encrypted, decrypted []byte + for _, value := range testStrings { + if encrypted, err = encrypt(block, []byte(value)); err != nil { + t.Error(err) + } else { + if decrypted, err = decrypt(block, encrypted); err != nil { + t.Error(err) + } + if string(decrypted) != value { + t.Errorf("Expected %v, got %v.", value, string(decrypted)) + } + } + } +} + +func TestSerialization(t *testing.T) { + var ( + serialized []byte + deserialized map[string]string + err error + ) + for _, value := range testCookies { + if serialized, err = serialize(value); err != nil { + t.Error(err) + } else { + deserialized = make(map[string]string) + if err = deserialize(serialized, &deserialized); err != nil { + t.Error(err) + } + if fmt.Sprintf("%v", deserialized) != fmt.Sprintf("%v", value) { + t.Errorf("Expected %v, got %v.", value, deserialized) + } + } + } +} + +func TestEncoding(t *testing.T) { + for _, value := range testStrings { + encoded := encode([]byte(value)) + decoded, err := decode(encoded) + if err != nil { + t.Error(err) + } else if string(decoded) != value { + t.Errorf("Expected %v, got %s.", value, string(decoded)) + } + } +} + +// ---------------------------------------------------------------------------- + +type FooBar struct { + Foo int + Bar string +} + +func TestCustomType(t *testing.T) { + s1 := New([]byte("12345"), []byte("1234567890123456")) + // Type is not registered in gob. (!!!) + src := &FooBar{42, "bar"} + encoded, _ := s1.Encode("sid", src) + + dst := &FooBar{} + _ = s1.Decode("sid", encoded, dst) + if dst.Foo != 42 || dst.Bar != "bar" { + t.Fatalf("Expected %#v, got %#v", src, dst) + } +} |