├── .editorconfig ├── .github └── workflows │ ├── issues.yml │ ├── security.yml │ ├── test.yml │ └── verify.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── doc.go ├── go.mod ├── go.sum ├── securecookie.go └── securecookie_test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | ; https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | insert_final_newline = true 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [{Makefile,go.mod,go.sum,*.go,.gitmodules}] 13 | indent_style = tab 14 | indent_size = 4 15 | 16 | [*.md] 17 | indent_size = 4 18 | trim_trailing_whitespace = false 19 | 20 | eclint_indent_style = unset 21 | -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | # Add all the issues created to the project. 2 | name: Add issue or pull request to Project 3 | 4 | on: 5 | issues: 6 | types: 7 | - opened 8 | pull_request_target: 9 | types: 10 | - opened 11 | - reopened 12 | 13 | jobs: 14 | add-to-project: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Add issue to project 18 | uses: actions/add-to-project@v0.5.0 19 | with: 20 | project-url: https://github.com/orgs/gorilla/projects/4 21 | github-token: ${{ secrets.ADD_TO_PROJECT_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | name: Security 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | permissions: 10 | contents: read 11 | jobs: 12 | scan: 13 | strategy: 14 | matrix: 15 | go: ['1.20','1.21'] 16 | fail-fast: true 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup Go ${{ matrix.go }} 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: ${{ matrix.go }} 26 | cache: false 27 | 28 | - name: Run GoSec 29 | uses: securego/gosec@master 30 | with: 31 | args: -exclude-dir examples ./... 32 | 33 | - name: Run GoVulnCheck 34 | uses: golang/govulncheck-action@v1 35 | with: 36 | go-version-input: ${{ matrix.go }} 37 | go-package: ./... 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | permissions: 10 | contents: read 11 | jobs: 12 | unit: 13 | strategy: 14 | matrix: 15 | go: ['1.20','1.21'] 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | fail-fast: true 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Go ${{ matrix.go }} 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: ${{ matrix.go }} 27 | cache: false 28 | 29 | - name: Run Tests 30 | run: go test -race -cover -coverprofile=coverage -covermode=atomic -v ./... 31 | 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v3 34 | with: 35 | files: ./coverage 36 | -------------------------------------------------------------------------------- /.github/workflows/verify.yml: -------------------------------------------------------------------------------- 1 | name: Verify 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | permissions: 10 | contents: read 11 | jobs: 12 | lint: 13 | strategy: 14 | matrix: 15 | go: ['1.20','1.21'] 16 | fail-fast: true 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup Go ${{ matrix.go }} 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: ${{ matrix.go }} 26 | cache: false 27 | 28 | - name: Run GolangCI-Lint 29 | uses: golangci/golangci-lint-action@v3 30 | with: 31 | version: v1.53 32 | args: --timeout=5m 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.coverprofile 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 The Gorilla Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_LINT=$(shell which golangci-lint 2> /dev/null || echo '') 2 | GO_LINT_URI=github.com/golangci/golangci-lint/cmd/golangci-lint@latest 3 | 4 | GO_SEC=$(shell which gosec 2> /dev/null || echo '') 5 | GO_SEC_URI=github.com/securego/gosec/v2/cmd/gosec@latest 6 | 7 | GO_VULNCHECK=$(shell which govulncheck 2> /dev/null || echo '') 8 | GO_VULNCHECK_URI=golang.org/x/vuln/cmd/govulncheck@latest 9 | 10 | .PHONY: golangci-lint 11 | golangci-lint: 12 | $(if $(GO_LINT), ,go install $(GO_LINT_URI)) 13 | @echo "##### Running golangci-lint" 14 | golangci-lint run -v 15 | 16 | .PHONY: gosec 17 | gosec: 18 | $(if $(GO_SEC), ,go install $(GO_SEC_URI)) 19 | @echo "##### Running gosec" 20 | gosec ./... 21 | 22 | .PHONY: govulncheck 23 | govulncheck: 24 | $(if $(GO_VULNCHECK), ,go install $(GO_VULNCHECK_URI)) 25 | @echo "##### Running govulncheck" 26 | govulncheck ./... 27 | 28 | .PHONY: verify 29 | verify: golangci-lint gosec govulncheck 30 | 31 | .PHONY: test 32 | test: 33 | @echo "##### Running tests" 34 | go test -race -cover -coverprofile=coverage.coverprofile -covermode=atomic -v ./... 35 | 36 | .PHONY: fuzz 37 | fuzz: 38 | @echo "##### Running fuzz tests" 39 | go test -v -fuzz FuzzEncodeDecode -fuzztime 60s 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gorilla/securecookie 2 | 3 | ![testing](https://github.com/gorilla/securecookie/actions/workflows/test.yml/badge.svg) 4 | [![codecov](https://codecov.io/github/gorilla/securecookie/branch/main/graph/badge.svg)](https://codecov.io/github/gorilla/securecookie) 5 | [![godoc](https://godoc.org/github.com/gorilla/securecookie?status.svg)](https://godoc.org/github.com/gorilla/securecookie) 6 | [![sourcegraph](https://sourcegraph.com/github.com/gorilla/securecookie/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/securecookie?badge) 7 | 8 | ![Gorilla Logo](https://github.com/gorilla/.github/assets/53367916/d92caabf-98e0-473e-bfbf-ab554ba435e5) 9 | 10 | securecookie encodes and decodes authenticated and optionally encrypted 11 | cookie values. 12 | 13 | Secure cookies can't be forged, because their values are validated using HMAC. 14 | When encrypted, the content is also inaccessible to malicious eyes. It is still 15 | recommended that sensitive data not be stored in cookies, and that HTTPS be used 16 | to prevent cookie [replay attacks](https://en.wikipedia.org/wiki/Replay_attack). 17 | 18 | ## Examples 19 | 20 | To use it, first create a new SecureCookie instance: 21 | 22 | ```go 23 | // Hash keys should be at least 32 bytes long 24 | var hashKey = []byte("very-secret") 25 | // Block keys should be 16 bytes (AES-128) or 32 bytes (AES-256) long. 26 | // Shorter keys may weaken the encryption used. 27 | var blockKey = []byte("a-lot-secret") 28 | var s = securecookie.New(hashKey, blockKey) 29 | ``` 30 | 31 | The hashKey is required, used to authenticate the cookie value using HMAC. 32 | It is recommended to use a key with 32 or 64 bytes. 33 | 34 | The blockKey is optional, used to encrypt the cookie value -- set it to nil 35 | to not use encryption. If set, the length must correspond to the block size 36 | of the encryption algorithm. For AES, used by default, valid lengths are 37 | 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. 38 | 39 | Strong keys can be created using the convenience function 40 | `GenerateRandomKey()`. Note that keys created using `GenerateRandomKey()` are not 41 | automatically persisted. New keys will be created when the application is 42 | restarted, and previously issued cookies will not be able to be decoded. 43 | 44 | Once a SecureCookie instance is set, use it to encode a cookie value: 45 | 46 | ```go 47 | func SetCookieHandler(w http.ResponseWriter, r *http.Request) { 48 | value := map[string]string{ 49 | "foo": "bar", 50 | } 51 | if encoded, err := s.Encode("cookie-name", value); err == nil { 52 | cookie := &http.Cookie{ 53 | Name: "cookie-name", 54 | Value: encoded, 55 | Path: "/", 56 | Secure: true, 57 | HttpOnly: true, 58 | } 59 | http.SetCookie(w, cookie) 60 | } 61 | } 62 | ``` 63 | 64 | Later, use the same SecureCookie instance to decode and validate a cookie 65 | value: 66 | 67 | ```go 68 | func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { 69 | if cookie, err := r.Cookie("cookie-name"); err == nil { 70 | value := make(map[string]string) 71 | if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { 72 | fmt.Fprintf(w, "The value of foo is %q", value["foo"]) 73 | } 74 | } 75 | } 76 | ``` 77 | 78 | We stored a map[string]string, but secure cookies can hold any value that 79 | can be encoded using `encoding/gob`. To store custom types, they must be 80 | registered first using gob.Register(). For basic types this is not needed; 81 | it works out of the box. An optional JSON encoder that uses `encoding/json` is 82 | available for types compatible with JSON. 83 | 84 | ### Key Rotation 85 | Rotating keys is an important part of any security strategy. The `EncodeMulti` and 86 | `DecodeMulti` functions allow for multiple keys to be rotated in and out. 87 | For example, let's take a system that stores keys in a map: 88 | 89 | ```go 90 | // keys stored in a map will not be persisted between restarts 91 | // a more persistent storage should be considered for production applications. 92 | var cookies = map[string]*securecookie.SecureCookie{ 93 | "previous": securecookie.New( 94 | securecookie.GenerateRandomKey(64), 95 | securecookie.GenerateRandomKey(32), 96 | ), 97 | "current": securecookie.New( 98 | securecookie.GenerateRandomKey(64), 99 | securecookie.GenerateRandomKey(32), 100 | ), 101 | } 102 | ``` 103 | 104 | Using the current key to encode new cookies: 105 | ```go 106 | func SetCookieHandler(w http.ResponseWriter, r *http.Request) { 107 | value := map[string]string{ 108 | "foo": "bar", 109 | } 110 | if encoded, err := securecookie.EncodeMulti("cookie-name", value, cookies["current"]); err == nil { 111 | cookie := &http.Cookie{ 112 | Name: "cookie-name", 113 | Value: encoded, 114 | Path: "/", 115 | } 116 | http.SetCookie(w, cookie) 117 | } 118 | } 119 | ``` 120 | 121 | Later, decode cookies. Check against all valid keys: 122 | ```go 123 | func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { 124 | if cookie, err := r.Cookie("cookie-name"); err == nil { 125 | value := make(map[string]string) 126 | err = securecookie.DecodeMulti("cookie-name", cookie.Value, &value, cookies["current"], cookies["previous"]) 127 | if err == nil { 128 | fmt.Fprintf(w, "The value of foo is %q", value["foo"]) 129 | } 130 | } 131 | } 132 | ``` 133 | 134 | Rotate the keys. This strategy allows previously issued cookies to be valid until the next rotation: 135 | ```go 136 | func Rotate(newCookie *securecookie.SecureCookie) { 137 | cookies["previous"] = cookies["current"] 138 | cookies["current"] = newCookie 139 | } 140 | ``` 141 | 142 | ## License 143 | 144 | BSD licensed. See the LICENSE file for details. 145 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | /* 6 | Package securecookie encodes and decodes authenticated and optionally 7 | encrypted cookie values. 8 | 9 | Secure cookies can't be forged, because their values are validated using HMAC. 10 | When encrypted, the content is also inaccessible to malicious eyes. 11 | 12 | To use it, first create a new SecureCookie instance: 13 | 14 | var hashKey = []byte("very-secret") 15 | var blockKey = []byte("a-lot-secret") 16 | var s = securecookie.New(hashKey, blockKey) 17 | 18 | The hashKey is required, used to authenticate the cookie value using HMAC. 19 | It is recommended to use a key with 32 or 64 bytes. 20 | 21 | The blockKey is optional, used to encrypt the cookie value -- set it to nil 22 | to not use encryption. If set, the length must correspond to the block size 23 | of the encryption algorithm. For AES, used by default, valid lengths are 24 | 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. 25 | 26 | Strong keys can be created using the convenience function GenerateRandomKey(). 27 | 28 | Once a SecureCookie instance is set, use it to encode a cookie value: 29 | 30 | func SetCookieHandler(w http.ResponseWriter, r *http.Request) { 31 | value := map[string]string{ 32 | "foo": "bar", 33 | } 34 | if encoded, err := s.Encode("cookie-name", value); err == nil { 35 | cookie := &http.Cookie{ 36 | Name: "cookie-name", 37 | Value: encoded, 38 | Path: "/", 39 | } 40 | http.SetCookie(w, cookie) 41 | } 42 | } 43 | 44 | Later, use the same SecureCookie instance to decode and validate a cookie 45 | value: 46 | 47 | func ReadCookieHandler(w http.ResponseWriter, r *http.Request) { 48 | if cookie, err := r.Cookie("cookie-name"); err == nil { 49 | value := make(map[string]string) 50 | if err = s2.Decode("cookie-name", cookie.Value, &value); err == nil { 51 | fmt.Fprintf(w, "The value of foo is %q", value["foo"]) 52 | } 53 | } 54 | } 55 | 56 | We stored a map[string]string, but secure cookies can hold any value that 57 | can be encoded using encoding/gob. To store custom types, they must be 58 | registered first using gob.Register(). For basic types this is not needed; 59 | it works out of the box. 60 | */ 61 | package securecookie 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gorilla/securecookie 2 | 3 | go 1.20 4 | 5 | require github.com/google/gofuzz v1.2.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 2 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 3 | -------------------------------------------------------------------------------- /securecookie.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package securecookie 6 | 7 | import ( 8 | "bytes" 9 | "crypto/aes" 10 | "crypto/cipher" 11 | "crypto/hmac" 12 | "crypto/rand" 13 | "crypto/sha256" 14 | "crypto/subtle" 15 | "encoding/base64" 16 | "encoding/gob" 17 | "encoding/json" 18 | "fmt" 19 | "hash" 20 | "io" 21 | "strconv" 22 | "strings" 23 | "time" 24 | ) 25 | 26 | // Error is the interface of all errors returned by functions in this library. 27 | type Error interface { 28 | error 29 | 30 | // IsUsage returns true for errors indicating the client code probably 31 | // uses this library incorrectly. For example, the client may have 32 | // failed to provide a valid hash key, or may have failed to configure 33 | // the Serializer adequately for encoding value. 34 | IsUsage() bool 35 | 36 | // IsDecode returns true for errors indicating that a cookie could not 37 | // be decoded and validated. Since cookies are usually untrusted 38 | // user-provided input, errors of this type should be expected. 39 | // Usually, the proper action is simply to reject the request. 40 | IsDecode() bool 41 | 42 | // IsInternal returns true for unexpected errors occurring in the 43 | // securecookie implementation. 44 | IsInternal() bool 45 | 46 | // Cause, if it returns a non-nil value, indicates that this error was 47 | // propagated from some underlying library. If this method returns nil, 48 | // this error was raised directly by this library. 49 | // 50 | // Cause is provided principally for debugging/logging purposes; it is 51 | // rare that application logic should perform meaningfully different 52 | // logic based on Cause. See, for example, the caveats described on 53 | // (MultiError).Cause(). 54 | Cause() error 55 | } 56 | 57 | // errorType is a bitmask giving the error type(s) of an cookieError value. 58 | type errorType int 59 | 60 | const ( 61 | usageError = errorType(1 << iota) 62 | decodeError 63 | internalError 64 | ) 65 | 66 | type cookieError struct { 67 | typ errorType 68 | msg string 69 | cause error 70 | } 71 | 72 | func (e cookieError) IsUsage() bool { return (e.typ & usageError) != 0 } 73 | func (e cookieError) IsDecode() bool { return (e.typ & decodeError) != 0 } 74 | func (e cookieError) IsInternal() bool { return (e.typ & internalError) != 0 } 75 | 76 | func (e cookieError) Cause() error { return e.cause } 77 | 78 | func (e cookieError) Error() string { 79 | parts := []string{"securecookie: "} 80 | if e.msg == "" { 81 | parts = append(parts, "error") 82 | } else { 83 | parts = append(parts, e.msg) 84 | } 85 | if c := e.Cause(); c != nil { 86 | parts = append(parts, " - caused by: ", c.Error()) 87 | } 88 | return strings.Join(parts, "") 89 | } 90 | 91 | var ( 92 | errGeneratingIV = cookieError{typ: internalError, msg: "failed to generate random iv"} 93 | 94 | errNoCodecs = cookieError{typ: usageError, msg: "no codecs provided"} 95 | errHashKeyNotSet = cookieError{typ: usageError, msg: "hash key is not set"} 96 | errBlockKeyNotSet = cookieError{typ: usageError, msg: "block key is not set"} 97 | errEncodedValueTooLong = cookieError{typ: usageError, msg: "the value is too long"} 98 | 99 | errValueToDecodeTooLong = cookieError{typ: decodeError, msg: "the value is too long"} 100 | errTimestampInvalid = cookieError{typ: decodeError, msg: "invalid timestamp"} 101 | errTimestampTooNew = cookieError{typ: decodeError, msg: "timestamp is too new"} 102 | errTimestampExpired = cookieError{typ: decodeError, msg: "expired timestamp"} 103 | errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"} 104 | errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."} 105 | errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."} 106 | 107 | // ErrMacInvalid indicates that cookie decoding failed because the HMAC 108 | // could not be extracted and verified. Direct use of this error 109 | // variable is deprecated; it is public only for legacy compatibility, 110 | // and may be privatized in the future, as it is rarely useful to 111 | // distinguish between this error and other Error implementations. 112 | ErrMacInvalid = cookieError{typ: decodeError, msg: "the value is not valid"} 113 | ) 114 | 115 | // Codec defines an interface to encode and decode cookie values. 116 | type Codec interface { 117 | Encode(name string, value interface{}) (string, error) 118 | Decode(name, value string, dst interface{}) error 119 | } 120 | 121 | // New returns a new SecureCookie. 122 | // 123 | // hashKey is required, used to authenticate values using HMAC. Create it using 124 | // GenerateRandomKey(). It is recommended to use a key with 32 or 64 bytes. 125 | // 126 | // blockKey is optional, used to encrypt values. Create it using 127 | // GenerateRandomKey(). The key length must correspond to the key size 128 | // of the encryption algorithm. For AES, used by default, valid lengths are 129 | // 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256. 130 | // The default encoder used for cookie serialization is encoding/gob. 131 | // 132 | // Note that keys created using GenerateRandomKey() are not automatically 133 | // persisted. New keys will be created when the application is restarted, and 134 | // previously issued cookies will not be able to be decoded. 135 | func New(hashKey, blockKey []byte) *SecureCookie { 136 | s := &SecureCookie{ 137 | hashKey: hashKey, 138 | blockKey: blockKey, 139 | hashFunc: sha256.New, 140 | maxAge: 86400 * 30, 141 | maxLength: 4096, 142 | sz: GobEncoder{}, 143 | } 144 | if len(hashKey) == 0 { 145 | s.err = errHashKeyNotSet 146 | } 147 | if blockKey != nil { 148 | s.BlockFunc(aes.NewCipher) 149 | } 150 | return s 151 | } 152 | 153 | // SecureCookie encodes and decodes authenticated and optionally encrypted 154 | // cookie values. 155 | type SecureCookie struct { 156 | hashKey []byte 157 | hashFunc func() hash.Hash 158 | blockKey []byte 159 | block cipher.Block 160 | maxLength int 161 | maxAge int64 162 | minAge int64 163 | err error 164 | sz Serializer 165 | // For testing purposes, the function that returns the current timestamp. 166 | // If not set, it will use time.Now().UTC().Unix(). 167 | timeFunc func() int64 168 | } 169 | 170 | // Serializer provides an interface for providing custom serializers for cookie 171 | // values. 172 | type Serializer interface { 173 | Serialize(src interface{}) ([]byte, error) 174 | Deserialize(src []byte, dst interface{}) error 175 | } 176 | 177 | // GobEncoder encodes cookie values using encoding/gob. This is the simplest 178 | // encoder and can handle complex types via gob.Register. 179 | type GobEncoder struct{} 180 | 181 | // JSONEncoder encodes cookie values using encoding/json. Users who wish to 182 | // encode complex types need to satisfy the json.Marshaller and 183 | // json.Unmarshaller interfaces. 184 | type JSONEncoder struct{} 185 | 186 | // NopEncoder does not encode cookie values, and instead simply accepts a []byte 187 | // (as an interface{}) and returns a []byte. This is particularly useful when 188 | // you encoding an object upstream and do not wish to re-encode it. 189 | type NopEncoder struct{} 190 | 191 | // MaxLength restricts the maximum length, in bytes, for the cookie value. 192 | // 193 | // Default is 4096, which is the maximum value accepted by Internet Explorer. 194 | func (s *SecureCookie) MaxLength(value int) *SecureCookie { 195 | s.maxLength = value 196 | return s 197 | } 198 | 199 | // MaxAge restricts the maximum age, in seconds, for the cookie value. 200 | // 201 | // Default is 86400 * 30. Set it to 0 for no restriction. 202 | func (s *SecureCookie) MaxAge(value int) *SecureCookie { 203 | s.maxAge = int64(value) 204 | return s 205 | } 206 | 207 | // MinAge restricts the minimum age, in seconds, for the cookie value. 208 | // 209 | // Default is 0 (no restriction). 210 | func (s *SecureCookie) MinAge(value int) *SecureCookie { 211 | s.minAge = int64(value) 212 | return s 213 | } 214 | 215 | // HashFunc sets the hash function used to create HMAC. 216 | // 217 | // Default is crypto/sha256.New. 218 | func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie { 219 | s.hashFunc = f 220 | return s 221 | } 222 | 223 | // BlockFunc sets the encryption function used to create a cipher.Block. 224 | // 225 | // Default is crypto/aes.New. 226 | func (s *SecureCookie) BlockFunc(f func([]byte) (cipher.Block, error)) *SecureCookie { 227 | if s.blockKey == nil { 228 | s.err = errBlockKeyNotSet 229 | } else if block, err := f(s.blockKey); err == nil { 230 | s.block = block 231 | } else { 232 | s.err = cookieError{cause: err, typ: usageError} 233 | } 234 | return s 235 | } 236 | 237 | // Encoding sets the encoding/serialization method for cookies. 238 | // 239 | // Default is encoding/gob. To encode special structures using encoding/gob, 240 | // they must be registered first using gob.Register(). 241 | func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie { 242 | s.sz = sz 243 | 244 | return s 245 | } 246 | 247 | // Encode encodes a cookie value. 248 | // 249 | // It serializes, optionally encrypts, signs with a message authentication code, 250 | // and finally encodes the value. 251 | // 252 | // The name argument is the cookie name. It is stored with the encoded value. 253 | // The value argument is the value to be encoded. It can be any value that can 254 | // be encoded using the currently selected serializer; see SetSerializer(). 255 | // 256 | // It is the client's responsibility to ensure that value, when encoded using 257 | // the current serialization/encryption settings on s and then base64-encoded, 258 | // is shorter than the maximum permissible length. 259 | func (s *SecureCookie) Encode(name string, value interface{}) (string, error) { 260 | if s.err != nil { 261 | return "", s.err 262 | } 263 | if s.hashKey == nil { 264 | s.err = errHashKeyNotSet 265 | return "", s.err 266 | } 267 | var err error 268 | var b []byte 269 | // 1. Serialize. 270 | if b, err = s.sz.Serialize(value); err != nil { 271 | return "", cookieError{cause: err, typ: usageError} 272 | } 273 | // 2. Encrypt (optional). 274 | if s.block != nil { 275 | if b, err = encrypt(s.block, b); err != nil { 276 | return "", cookieError{cause: err, typ: usageError} 277 | } 278 | } 279 | b = encode(b) 280 | // 3. Create MAC for "name|date|value". Extra pipe to be used later. 281 | b = []byte(fmt.Sprintf("%s|%d|%s|", name, s.timestamp(), b)) 282 | mac := createMac(hmac.New(s.hashFunc, s.hashKey), b[:len(b)-1]) 283 | // Append mac, remove name. 284 | b = append(b, mac...)[len(name)+1:] 285 | // 4. Encode to base64. 286 | b = encode(b) 287 | // 5. Check length. 288 | if s.maxLength != 0 && len(b) > s.maxLength { 289 | return "", fmt.Errorf("%s: %d", errEncodedValueTooLong, len(b)) 290 | } 291 | // Done. 292 | return string(b), nil 293 | } 294 | 295 | // Decode decodes a cookie value. 296 | // 297 | // It decodes, verifies a message authentication code, optionally decrypts and 298 | // finally deserializes the value. 299 | // 300 | // The name argument is the cookie name. It must be the same name used when 301 | // it was stored. The value argument is the encoded cookie value. The dst 302 | // argument is where the cookie will be decoded. It must be a pointer. 303 | func (s *SecureCookie) Decode(name, value string, dst interface{}) error { 304 | if s.err != nil { 305 | return s.err 306 | } 307 | if s.hashKey == nil { 308 | s.err = errHashKeyNotSet 309 | return s.err 310 | } 311 | // 1. Check length. 312 | if s.maxLength != 0 && len(value) > s.maxLength { 313 | return fmt.Errorf("%s: %d", errValueToDecodeTooLong, len(value)) 314 | } 315 | // 2. Decode from base64. 316 | b, err := decode([]byte(value)) 317 | if err != nil { 318 | return err 319 | } 320 | // 3. Verify MAC. Value is "date|value|mac". 321 | parts := bytes.SplitN(b, []byte("|"), 3) 322 | if len(parts) != 3 { 323 | return ErrMacInvalid 324 | } 325 | h := hmac.New(s.hashFunc, s.hashKey) 326 | b = append([]byte(name+"|"), b[:len(b)-len(parts[2])-1]...) 327 | if err = verifyMac(h, b, parts[2]); err != nil { 328 | return err 329 | } 330 | // 4. Verify date ranges. 331 | var t1 int64 332 | if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil { 333 | return errTimestampInvalid 334 | } 335 | t2 := s.timestamp() 336 | if s.minAge != 0 && t1 > t2-s.minAge { 337 | return errTimestampTooNew 338 | } 339 | if s.maxAge != 0 && t1 < t2-s.maxAge { 340 | return errTimestampExpired 341 | } 342 | // 5. Decrypt (optional). 343 | b, err = decode(parts[1]) 344 | if err != nil { 345 | return err 346 | } 347 | if s.block != nil { 348 | if b, err = decrypt(s.block, b); err != nil { 349 | return err 350 | } 351 | } 352 | // 6. Deserialize. 353 | if err = s.sz.Deserialize(b, dst); err != nil { 354 | return cookieError{cause: err, typ: decodeError} 355 | } 356 | // Done. 357 | return nil 358 | } 359 | 360 | // timestamp returns the current timestamp, in seconds. 361 | // 362 | // For testing purposes, the function that generates the timestamp can be 363 | // overridden. If not set, it will return time.Now().UTC().Unix(). 364 | func (s *SecureCookie) timestamp() int64 { 365 | if s.timeFunc == nil { 366 | return time.Now().UTC().Unix() 367 | } 368 | return s.timeFunc() 369 | } 370 | 371 | // Authentication ------------------------------------------------------------- 372 | 373 | // createMac creates a message authentication code (MAC). 374 | func createMac(h hash.Hash, value []byte) []byte { 375 | h.Write(value) 376 | return h.Sum(nil) 377 | } 378 | 379 | // verifyMac verifies that a message authentication code (MAC) is valid. 380 | func verifyMac(h hash.Hash, value []byte, mac []byte) error { 381 | mac2 := createMac(h, value) 382 | // Check that both MACs are of equal length, as subtle.ConstantTimeCompare 383 | // does not do this prior to Go 1.4. 384 | if len(mac) == len(mac2) && subtle.ConstantTimeCompare(mac, mac2) == 1 { 385 | return nil 386 | } 387 | return ErrMacInvalid 388 | } 389 | 390 | // Encryption ----------------------------------------------------------------- 391 | 392 | // encrypt encrypts a value using the given block in counter mode. 393 | // 394 | // A random initialization vector ( https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_(IV) ) with the length of the 395 | // block size is prepended to the resulting ciphertext. 396 | func encrypt(block cipher.Block, value []byte) ([]byte, error) { 397 | iv := GenerateRandomKey(block.BlockSize()) 398 | if iv == nil { 399 | return nil, errGeneratingIV 400 | } 401 | // Encrypt it. 402 | stream := cipher.NewCTR(block, iv) 403 | stream.XORKeyStream(value, value) 404 | // Return iv + ciphertext. 405 | return append(iv, value...), nil 406 | } 407 | 408 | // decrypt decrypts a value using the given block in counter mode. 409 | // 410 | // The value to be decrypted must be prepended by a initialization vector 411 | // ( https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_(IV) ) with the length of the block size. 412 | func decrypt(block cipher.Block, value []byte) ([]byte, error) { 413 | size := block.BlockSize() 414 | if len(value) > size { 415 | // Extract iv. 416 | iv := value[:size] 417 | // Extract ciphertext. 418 | value = value[size:] 419 | // Decrypt it. 420 | stream := cipher.NewCTR(block, iv) 421 | stream.XORKeyStream(value, value) 422 | return value, nil 423 | } 424 | return nil, errDecryptionFailed 425 | } 426 | 427 | // Serialization -------------------------------------------------------------- 428 | 429 | // Serialize encodes a value using gob. 430 | func (e GobEncoder) Serialize(src interface{}) ([]byte, error) { 431 | buf := new(bytes.Buffer) 432 | enc := gob.NewEncoder(buf) 433 | if err := enc.Encode(src); err != nil { 434 | return nil, cookieError{cause: err, typ: usageError} 435 | } 436 | return buf.Bytes(), nil 437 | } 438 | 439 | // Deserialize decodes a value using gob. 440 | func (e GobEncoder) Deserialize(src []byte, dst interface{}) error { 441 | dec := gob.NewDecoder(bytes.NewBuffer(src)) 442 | if err := dec.Decode(dst); err != nil { 443 | return cookieError{cause: err, typ: decodeError} 444 | } 445 | return nil 446 | } 447 | 448 | // Serialize encodes a value using encoding/json. 449 | func (e JSONEncoder) Serialize(src interface{}) ([]byte, error) { 450 | buf := new(bytes.Buffer) 451 | enc := json.NewEncoder(buf) 452 | if err := enc.Encode(src); err != nil { 453 | return nil, cookieError{cause: err, typ: usageError} 454 | } 455 | return buf.Bytes(), nil 456 | } 457 | 458 | // Deserialize decodes a value using encoding/json. 459 | func (e JSONEncoder) Deserialize(src []byte, dst interface{}) error { 460 | dec := json.NewDecoder(bytes.NewReader(src)) 461 | if err := dec.Decode(dst); err != nil { 462 | return cookieError{cause: err, typ: decodeError} 463 | } 464 | return nil 465 | } 466 | 467 | // Serialize passes a []byte through as-is. 468 | func (e NopEncoder) Serialize(src interface{}) ([]byte, error) { 469 | if b, ok := src.([]byte); ok { 470 | return b, nil 471 | } 472 | 473 | return nil, errValueNotByte 474 | } 475 | 476 | // Deserialize passes a []byte through as-is. 477 | func (e NopEncoder) Deserialize(src []byte, dst interface{}) error { 478 | if dat, ok := dst.(*[]byte); ok { 479 | *dat = src 480 | return nil 481 | } 482 | return errValueNotBytePtr 483 | } 484 | 485 | // Encoding ------------------------------------------------------------------- 486 | 487 | // encode encodes a value using base64. 488 | func encode(value []byte) []byte { 489 | encoded := make([]byte, base64.URLEncoding.EncodedLen(len(value))) 490 | base64.URLEncoding.Encode(encoded, value) 491 | return encoded 492 | } 493 | 494 | // decode decodes a cookie using base64. 495 | func decode(value []byte) ([]byte, error) { 496 | decoded := make([]byte, base64.URLEncoding.DecodedLen(len(value))) 497 | b, err := base64.URLEncoding.Decode(decoded, value) 498 | if err != nil { 499 | return nil, cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"} 500 | } 501 | return decoded[:b], nil 502 | } 503 | 504 | // Helpers -------------------------------------------------------------------- 505 | 506 | // GenerateRandomKey creates a random key with the given length in bytes. 507 | // On failure, returns nil. 508 | // 509 | // Note that keys created using `GenerateRandomKey()` are not automatically 510 | // persisted. New keys will be created when the application is restarted, and 511 | // previously issued cookies will not be able to be decoded. 512 | // 513 | // Callers should explicitly check for the possibility of a nil return, treat 514 | // it as a failure of the system random number generator, and not continue. 515 | func GenerateRandomKey(length int) []byte { 516 | k := make([]byte, length) 517 | if _, err := io.ReadFull(rand.Reader, k); err != nil { 518 | return nil 519 | } 520 | return k 521 | } 522 | 523 | // CodecsFromPairs returns a slice of SecureCookie instances. 524 | // 525 | // It is a convenience function to create a list of codecs for key rotation. Note 526 | // that the generated Codecs will have the default options applied: callers 527 | // should iterate over each Codec and type-assert the underlying *SecureCookie to 528 | // change these. 529 | // 530 | // Example: 531 | // 532 | // codecs := securecookie.CodecsFromPairs( 533 | // []byte("new-hash-key"), 534 | // []byte("new-block-key"), 535 | // []byte("old-hash-key"), 536 | // []byte("old-block-key"), 537 | // ) 538 | // 539 | // // Modify each instance. 540 | // for _, s := range codecs { 541 | // if cookie, ok := s.(*securecookie.SecureCookie); ok { 542 | // cookie.MaxAge(86400 * 7) 543 | // cookie.SetSerializer(securecookie.JSONEncoder{}) 544 | // cookie.HashFunc(sha512.New512_256) 545 | // } 546 | // } 547 | func CodecsFromPairs(keyPairs ...[]byte) []Codec { 548 | codecs := make([]Codec, len(keyPairs)/2+len(keyPairs)%2) 549 | for i := 0; i < len(keyPairs); i += 2 { 550 | var blockKey []byte 551 | if i+1 < len(keyPairs) { 552 | blockKey = keyPairs[i+1] 553 | } 554 | codecs[i/2] = New(keyPairs[i], blockKey) 555 | } 556 | return codecs 557 | } 558 | 559 | // EncodeMulti encodes a cookie value using a group of codecs. 560 | // 561 | // The codecs are tried in order. Multiple codecs are accepted to allow 562 | // key rotation. 563 | // 564 | // On error, may return a MultiError. 565 | func EncodeMulti(name string, value interface{}, codecs ...Codec) (string, error) { 566 | if len(codecs) == 0 { 567 | return "", errNoCodecs 568 | } 569 | 570 | var errors MultiError 571 | for _, codec := range codecs { 572 | encoded, err := codec.Encode(name, value) 573 | if err == nil { 574 | return encoded, nil 575 | } 576 | errors = append(errors, err) 577 | } 578 | return "", errors 579 | } 580 | 581 | // DecodeMulti decodes a cookie value using a group of codecs. 582 | // 583 | // The codecs are tried in order. Multiple codecs are accepted to allow 584 | // key rotation. 585 | // 586 | // On error, may return a MultiError. 587 | func DecodeMulti(name string, value string, dst interface{}, codecs ...Codec) error { 588 | if len(codecs) == 0 { 589 | return errNoCodecs 590 | } 591 | 592 | var errors MultiError 593 | for _, codec := range codecs { 594 | err := codec.Decode(name, value, dst) 595 | if err == nil { 596 | return nil 597 | } 598 | errors = append(errors, err) 599 | } 600 | return errors 601 | } 602 | 603 | // MultiError groups multiple errors. 604 | type MultiError []error 605 | 606 | func (m MultiError) IsUsage() bool { return m.any(func(e Error) bool { return e.IsUsage() }) } 607 | func (m MultiError) IsDecode() bool { return m.any(func(e Error) bool { return e.IsDecode() }) } 608 | func (m MultiError) IsInternal() bool { return m.any(func(e Error) bool { return e.IsInternal() }) } 609 | 610 | // Cause returns nil for MultiError; there is no unique underlying cause in the 611 | // general case. 612 | // 613 | // Note: we could conceivably return a non-nil Cause only when there is exactly 614 | // one child error with a Cause. However, it would be brittle for client code 615 | // to rely on the arity of causes inside a MultiError, so we have opted not to 616 | // provide this functionality. Clients which really wish to access the Causes 617 | // of the underlying errors are free to iterate through the errors themselves. 618 | func (m MultiError) Cause() error { return nil } 619 | 620 | func (m MultiError) Error() string { 621 | s, n := "", 0 622 | for _, e := range m { 623 | if e != nil { 624 | if n == 0 { 625 | s = e.Error() 626 | } 627 | n++ 628 | } 629 | } 630 | switch n { 631 | case 0: 632 | return "(0 errors)" 633 | case 1: 634 | return s 635 | case 2: 636 | return s + " (and 1 other error)" 637 | } 638 | return fmt.Sprintf("%s (and %d other errors)", s, n-1) 639 | } 640 | 641 | // any returns true if any element of m is an Error for which pred returns true. 642 | func (m MultiError) any(pred func(Error) bool) bool { 643 | for _, e := range m { 644 | if ourErr, ok := e.(Error); ok && pred(ourErr) { 645 | return true 646 | } 647 | } 648 | return false 649 | } 650 | -------------------------------------------------------------------------------- /securecookie_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 The Gorilla Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package securecookie 6 | 7 | import ( 8 | "crypto/aes" 9 | "crypto/hmac" 10 | "crypto/sha256" 11 | "encoding/base64" 12 | "fmt" 13 | "reflect" 14 | "strings" 15 | "testing" 16 | 17 | fuzz "github.com/google/gofuzz" 18 | ) 19 | 20 | // Asserts that cookieError and MultiError are Error implementations. 21 | var _ Error = cookieError{} 22 | var _ Error = MultiError{} 23 | 24 | var testCookies = []interface{}{ 25 | map[string]string{"foo": "bar"}, 26 | map[string]string{"baz": "ding"}, 27 | } 28 | 29 | var testStrings = []string{"foo", "bar", "baz"} 30 | 31 | func TestSecureCookie(t *testing.T) { 32 | // TODO test too old / too new timestamps 33 | s1 := New([]byte("12345"), []byte("1234567890123456")) 34 | s2 := New([]byte("54321"), []byte("6543210987654321")) 35 | value := map[string]interface{}{ 36 | "foo": "bar", 37 | "baz": 128, 38 | } 39 | 40 | for i := 0; i < 50; i++ { 41 | // Running this multiple times to check if any special character 42 | // breaks encoding/decoding. 43 | encoded, err1 := s1.Encode("sid", value) 44 | if err1 != nil { 45 | t.Error(err1) 46 | continue 47 | } 48 | dst := make(map[string]interface{}) 49 | err2 := s1.Decode("sid", encoded, &dst) 50 | if err2 != nil { 51 | t.Fatalf("%v: %v", err2, encoded) 52 | } 53 | if !reflect.DeepEqual(dst, value) { 54 | t.Fatalf("Expected %v, got %v.", value, dst) 55 | } 56 | dst2 := make(map[string]interface{}) 57 | err3 := s2.Decode("sid", encoded, &dst2) 58 | if err3 == nil { 59 | t.Fatalf("Expected failure decoding.") 60 | } 61 | err4, ok := err3.(Error) 62 | if !ok { 63 | t.Fatalf("Expected error to implement Error, got: %#v", err3) 64 | } 65 | if !err4.IsDecode() { 66 | t.Fatalf("Expected DecodeError, got: %#v", err4) 67 | } 68 | 69 | // Test other error type flags. 70 | if err4.IsUsage() { 71 | t.Fatalf("Expected IsUsage() == false, got: %#v", err4) 72 | } 73 | if err4.IsInternal() { 74 | t.Fatalf("Expected IsInternal() == false, got: %#v", err4) 75 | } 76 | } 77 | } 78 | 79 | func TestSecureCookieNilKey(t *testing.T) { 80 | s1 := New(nil, nil) 81 | value := map[string]interface{}{ 82 | "foo": "bar", 83 | "baz": 128, 84 | } 85 | _, err := s1.Encode("sid", value) 86 | if err != errHashKeyNotSet { 87 | t.Fatal("Wrong error returned:", err) 88 | } 89 | } 90 | 91 | func TestDecodeInvalid(t *testing.T) { 92 | // List of invalid cookies, which must not be accepted, base64-decoded 93 | // (they will be encoded before passing to Decode). 94 | invalidCookies := []string{ 95 | "", 96 | " ", 97 | "\n", 98 | "||", 99 | "|||", 100 | "cookie", 101 | } 102 | s := New([]byte("12345"), nil) 103 | var dst string 104 | for i, v := range invalidCookies { 105 | for _, enc := range []*base64.Encoding{ 106 | base64.StdEncoding, 107 | base64.URLEncoding, 108 | } { 109 | err := s.Decode("name", enc.EncodeToString([]byte(v)), &dst) 110 | if err == nil { 111 | t.Fatalf("%d: expected failure decoding", i) 112 | } 113 | err2, ok := err.(Error) 114 | if !ok || !err2.IsDecode() { 115 | t.Fatalf("%d: Expected IsDecode(), got: %#v", i, err) 116 | } 117 | } 118 | } 119 | } 120 | 121 | func TestAuthentication(t *testing.T) { 122 | hash := hmac.New(sha256.New, []byte("secret-key")) 123 | for _, value := range testStrings { 124 | hash.Reset() 125 | signed := createMac(hash, []byte(value)) 126 | hash.Reset() 127 | err := verifyMac(hash, []byte(value), signed) 128 | if err != nil { 129 | t.Error(err) 130 | } 131 | } 132 | } 133 | 134 | func TestEncryption(t *testing.T) { 135 | block, err := aes.NewCipher([]byte("1234567890123456")) 136 | if err != nil { 137 | t.Fatalf("Block could not be created") 138 | } 139 | var encrypted, decrypted []byte 140 | for _, value := range testStrings { 141 | if encrypted, err = encrypt(block, []byte(value)); err != nil { 142 | t.Error(err) 143 | } else { 144 | if decrypted, err = decrypt(block, encrypted); err != nil { 145 | t.Error(err) 146 | } 147 | if string(decrypted) != value { 148 | t.Errorf("Expected %v, got %v.", value, string(decrypted)) 149 | } 150 | } 151 | } 152 | } 153 | 154 | func TestGobSerialization(t *testing.T) { 155 | var ( 156 | sz GobEncoder 157 | serialized []byte 158 | deserialized map[string]string 159 | err error 160 | ) 161 | for _, value := range testCookies { 162 | if serialized, err = sz.Serialize(value); err != nil { 163 | t.Error(err) 164 | } else { 165 | deserialized = make(map[string]string) 166 | if err = sz.Deserialize(serialized, &deserialized); err != nil { 167 | t.Error(err) 168 | } 169 | if fmt.Sprintf("%v", deserialized) != fmt.Sprintf("%v", value) { 170 | t.Errorf("Expected %v, got %v.", value, deserialized) 171 | } 172 | } 173 | } 174 | } 175 | 176 | func TestJSONSerialization(t *testing.T) { 177 | var ( 178 | sz JSONEncoder 179 | serialized []byte 180 | deserialized map[string]string 181 | err error 182 | ) 183 | for _, value := range testCookies { 184 | if serialized, err = sz.Serialize(value); err != nil { 185 | t.Error(err) 186 | } else { 187 | deserialized = make(map[string]string) 188 | if err = sz.Deserialize(serialized, &deserialized); err != nil { 189 | t.Error(err) 190 | } 191 | if fmt.Sprintf("%v", deserialized) != fmt.Sprintf("%v", value) { 192 | t.Errorf("Expected %v, got %v.", value, deserialized) 193 | } 194 | } 195 | } 196 | } 197 | 198 | func TestNopSerialization(t *testing.T) { 199 | cookieData := "fooobar123" 200 | sz := NopEncoder{} 201 | 202 | if _, err := sz.Serialize(cookieData); err != errValueNotByte { 203 | t.Fatal("Expected error passing string") 204 | } 205 | dat, err := sz.Serialize([]byte(cookieData)) 206 | if err != nil { 207 | t.Fatal(err) 208 | } 209 | if (string(dat)) != cookieData { 210 | t.Fatal("Expected serialized data to be same as source") 211 | } 212 | 213 | var dst []byte 214 | if err = sz.Deserialize(dat, dst); err != errValueNotBytePtr { 215 | t.Fatal("Expect error unless you pass a *[]byte") 216 | } 217 | if err = sz.Deserialize(dat, &dst); err != nil { 218 | t.Fatal(err) 219 | } 220 | if (string(dst)) != cookieData { 221 | t.Fatal("Expected deserialized data to be same as source") 222 | } 223 | } 224 | 225 | func TestEncoding(t *testing.T) { 226 | for _, value := range testStrings { 227 | encoded := encode([]byte(value)) 228 | decoded, err := decode(encoded) 229 | if err != nil { 230 | t.Error(err) 231 | } else if string(decoded) != value { 232 | t.Errorf("Expected %v, got %s.", value, string(decoded)) 233 | } 234 | } 235 | } 236 | 237 | func TestMultiError(t *testing.T) { 238 | s1, s2 := New(nil, nil), New(nil, nil) 239 | _, err := EncodeMulti("sid", "value", s1, s2) 240 | if len(err.(MultiError)) != 2 { 241 | t.Errorf("Expected 2 errors, got %s.", err) 242 | } else { 243 | if !strings.Contains(err.Error(), "hash key is not set") { 244 | t.Errorf("Expected missing hash key error, got %s.", err.Error()) 245 | } 246 | ourErr, ok := err.(Error) 247 | if !ok || !ourErr.IsUsage() { 248 | t.Fatalf("Expected error to be a usage error; got %#v", err) 249 | } 250 | if ourErr.IsDecode() { 251 | t.Errorf("Expected error NOT to be a decode error; got %#v", ourErr) 252 | } 253 | if ourErr.IsInternal() { 254 | t.Errorf("Expected error NOT to be an internal error; got %#v", ourErr) 255 | } 256 | } 257 | } 258 | 259 | func TestMultiNoCodecs(t *testing.T) { 260 | _, err := EncodeMulti("foo", "bar") 261 | if err != errNoCodecs { 262 | t.Errorf("EncodeMulti: bad value for error, got: %v", err) 263 | } 264 | 265 | var dst []byte 266 | err = DecodeMulti("foo", "bar", &dst) 267 | if err != errNoCodecs { 268 | t.Errorf("DecodeMulti: bad value for error, got: %v", err) 269 | } 270 | } 271 | 272 | func TestMissingKey(t *testing.T) { 273 | emptyKeys := [][]byte{ 274 | nil, 275 | []byte(""), 276 | } 277 | 278 | for _, key := range emptyKeys { 279 | s1 := New(key, nil) 280 | 281 | var dst []byte 282 | err := s1.Decode("sid", "value", &dst) 283 | if err != errHashKeyNotSet { 284 | t.Fatalf("Expected %#v, got %#v", errHashKeyNotSet, err) 285 | } 286 | if err2, ok := err.(Error); !ok || !err2.IsUsage() { 287 | t.Errorf("Expected missing hash key to be IsUsage(); was %#v", err) 288 | } 289 | } 290 | } 291 | 292 | // ---------------------------------------------------------------------------- 293 | 294 | type FooBar struct { 295 | Foo int 296 | Bar string 297 | } 298 | 299 | func TestCustomType(t *testing.T) { 300 | s1 := New([]byte("12345"), []byte("1234567890123456")) 301 | // Type is not registered in gob. (!!!) 302 | src := &FooBar{42, "bar"} 303 | encoded, _ := s1.Encode("sid", src) 304 | 305 | dst := &FooBar{} 306 | _ = s1.Decode("sid", encoded, dst) 307 | if dst.Foo != 42 || dst.Bar != "bar" { 308 | t.Fatalf("Expected %#v, got %#v", src, dst) 309 | } 310 | } 311 | 312 | type Cookie struct { 313 | B bool 314 | I int 315 | S string 316 | } 317 | 318 | func FuzzEncodeDecode(f *testing.F) { 319 | fuzzer := fuzz.New() 320 | s1 := New([]byte("12345"), []byte("1234567890123456")) 321 | s1.maxLength = 0 322 | 323 | for i := 0; i < 100000; i++ { 324 | var c Cookie 325 | fuzzer.Fuzz(&c) 326 | f.Add(c.B, c.I, c.S) 327 | } 328 | 329 | f.Fuzz(func(t *testing.T, b bool, i int, s string) { 330 | c := Cookie{b, i, s} 331 | encoded, err := s1.Encode("sid", c) 332 | if err != nil { 333 | t.Errorf("Encode failed: %v", err) 334 | } 335 | dc := Cookie{} 336 | err = s1.Decode("sid", encoded, &dc) 337 | if err != nil { 338 | t.Errorf("Decode failed: %v", err) 339 | } 340 | if dc != c { 341 | t.Fatalf("Expected %v, got %v.", s, dc) 342 | } 343 | }) 344 | } 345 | --------------------------------------------------------------------------------