Skip to content

add more compact encoding #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 96 additions & 26 deletions securecookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"encoding/gob"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -137,6 +138,7 @@ func New(hashKey, blockKey []byte) *SecureCookie {
hashKey: hashKey,
blockKey: blockKey,
hashFunc: sha256.New,
macSize: sha256.New().Size(),
maxAge: 86400 * 30,
maxLength: 4096,
sz: GobEncoder{},
Expand All @@ -155,12 +157,14 @@ func New(hashKey, blockKey []byte) *SecureCookie {
type SecureCookie struct {
hashKey []byte
hashFunc func() hash.Hash
macSize int
blockKey []byte
block cipher.Block
maxLength int
maxAge int64
minAge int64
err error
compact bool
sz Serializer
// For testing purposes, the function that returns the current timestamp.
// If not set, it will use time.Now().UTC().Unix().
Expand Down Expand Up @@ -217,6 +221,7 @@ func (s *SecureCookie) MinAge(value int) *SecureCookie {
// Default is crypto/sha256.New.
func (s *SecureCookie) HashFunc(f func() hash.Hash) *SecureCookie {
s.hashFunc = f
s.macSize = f().Size()
return s
}

Expand Down Expand Up @@ -244,6 +249,15 @@ func (s *SecureCookie) SetSerializer(sz Serializer) *SecureCookie {
return s
}

// Compact sets compact but backward incompatible encoding format.
//
// Default is false
func (s *SecureCookie) Compact(c bool) *SecureCookie {
s.compact = c

return s
}

// Encode encodes a cookie value.
//
// It serializes, optionally encrypts, signs with a message authentication code,
Expand Down Expand Up @@ -276,12 +290,22 @@ func (s *SecureCookie) Encode(name string, value interface{}) (string, error) {
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:]
if !s.compact {
b = encode(b)
// 3. Create MAC for "name|date|value". Extra pipe to be used later.
b = []byte(fmt.Sprintf("%d|%s|", s.timestamp(), b))
mac := createMac(s.createHMAC(false), name+"|", b[:len(b)-1])
// Append mac
b = append(b, mac...)
} else {
// 3. Create MAC for concatenation of name, date and value.
t := make([]byte, binary.MaxVarintLen64, binary.MaxVarintLen64+len(b)+s.compactMacSize())
tl := binary.PutVarint(t[:], s.timestamp())
b = append(t[:tl], b...)
mac := createMac(s.createHMAC(true), name, b)
// Append mac
b = append(b, mac...)
}
// 4. Encode to base64.
b = encode(b)
// 5. Check length.
Expand All @@ -292,6 +316,21 @@ func (s *SecureCookie) Encode(name string, value interface{}) (string, error) {
return string(b), nil
}

func (s *SecureCookie) createHMAC(compact bool) hash.Hash {
h := hmac.New(s.hashFunc, s.hashKey)
if compact && s.macSize > 16 {
return compactHash{h}
}
return h
}

func (s *SecureCookie) compactMacSize() int {
if s.macSize > 16 {
return 16
}
return s.macSize
}

// Decode decodes a cookie value.
//
// It decodes, verifies a message authentication code, optionally decrypts and
Expand All @@ -317,21 +356,41 @@ func (s *SecureCookie) Decode(name, value string, dst interface{}) error {
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
h := s.createHMAC(s.compact)
if !s.compact {
// 3. Verify MAC. Value is "date|value|mac".
parts := bytes.SplitN(b, []byte("|"), 3)
if len(parts) != 3 {
return ErrMacInvalid
}
b = b[:len(b)-len(parts[2])-1]
if err = verifyMac(h, name+"|", b, parts[2]); err != nil {
return err
}
// extract timestamp
if t1, err = strconv.ParseInt(string(parts[0]), 10, 64); err != nil {
return errTimestampInvalid
}
// extract payload
b, err = decode(parts[1])
if err != nil {
return err
}
} else {
macStart := len(b) - s.compactMacSize()
mac := b[macStart:]
b = b[:macStart]
if err = verifyMac(h, name, b, mac); err != nil {
return err
}
// extract timestamp
var tl int
t1, tl = binary.Varint(b)
// extract payload
b = b[tl:]
}
// 4. Verify date ranges.
t2 := s.timestamp()
if s.minAge != 0 && t1 > t2-s.minAge {
return errTimestampTooNew
Expand All @@ -340,10 +399,6 @@ func (s *SecureCookie) Decode(name, value string, dst interface{}) error {
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
Expand Down Expand Up @@ -371,14 +426,15 @@ func (s *SecureCookie) timestamp() int64 {
// Authentication -------------------------------------------------------------

// createMac creates a message authentication code (MAC).
func createMac(h hash.Hash, value []byte) []byte {
func createMac(h hash.Hash, prefix string, value []byte) []byte {
h.Write([]byte(prefix))
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)
func verifyMac(h hash.Hash, prefix string, value []byte, mac []byte) error {
mac2 := createMac(h, prefix, 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 {
Expand Down Expand Up @@ -648,3 +704,17 @@ func (m MultiError) any(pred func(Error) bool) bool {
}
return false
}

type compactHash struct {
hash.Hash
}

func (ch compactHash) Size() int {
return 16
}

func (ch compactHash) Sum(b []byte) []byte {
origLen := len(b)
b = ch.Hash.Sum(b)
return b[:origLen+16]
}
13 changes: 11 additions & 2 deletions securecookie_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"math/rand"
"reflect"
"strings"
"testing"
Expand All @@ -34,6 +35,7 @@ func TestSecureCookie(t *testing.T) {
"foo": "bar",
"baz": 128,
}
rng := rand.New(rand.NewSource(1))

for i := 0; i < 50; i++ {
// Running this multiple times to check if any special character
Expand All @@ -43,6 +45,7 @@ func TestSecureCookie(t *testing.T) {
t.Error(err1)
continue
}
t.Log("i", i, "len", len(encoded), "c", encoded)
dst := make(map[string]interface{})
err2 := s1.Decode("sid", encoded, &dst)
if err2 != nil {
Expand Down Expand Up @@ -71,6 +74,12 @@ func TestSecureCookie(t *testing.T) {
if err4.IsInternal() {
t.Fatalf("Expected IsInternal() == false, got: %#v", err4)
}

value["foo"] = string(append([]rune("bar"), rune(rng.Int31n(1024)+1)))
value["baz"] = rng.Intn(1000000)

s1.Compact(i&1 == 0)
s2.Compact(i&2 == 0)
}
}

Expand Down Expand Up @@ -120,9 +129,9 @@ func TestAuthentication(t *testing.T) {
hash := hmac.New(sha256.New, []byte("secret-key"))
for _, value := range testStrings {
hash.Reset()
signed := createMac(hash, []byte(value))
signed := createMac(hash, "prefix", []byte(value))
hash.Reset()
err := verifyMac(hash, []byte(value), signed)
err := verifyMac(hash, "prefix", []byte(value), signed)
if err != nil {
t.Error(err)
}
Expand Down