diff --git a/securecookie.go b/securecookie.go index b718ce9..5f98fb5 100644 --- a/securecookie.go +++ b/securecookie.go @@ -13,6 +13,7 @@ import ( "crypto/sha256" "crypto/subtle" "encoding/base64" + "encoding/binary" "encoding/gob" "encoding/json" "fmt" @@ -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{}, @@ -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(). @@ -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 } @@ -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, @@ -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. @@ -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 @@ -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 @@ -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 @@ -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 { @@ -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] +} diff --git a/securecookie_test.go b/securecookie_test.go index c32ff33..95bbb01 100644 --- a/securecookie_test.go +++ b/securecookie_test.go @@ -10,6 +10,7 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "math/rand" "reflect" "strings" "testing" @@ -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 @@ -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 { @@ -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) } } @@ -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) }