From af6d2d6a6553b9cf9eafdd7f5b29e518a296d9d3 Mon Sep 17 00:00:00 2001 From: Sokolov Yura Date: Mon, 29 Jun 2020 06:31:20 +0300 Subject: [PATCH 1/2] add more compact encoding Current encoding does Base64 twice against payload because of intermediate text encoding. And it produces too large mac (256bit) while 128bit is more than enough. Add "compact" mode which uses binary encoding for message with single Base64 pass and 128bit hmac output (at max). --- securecookie.go | 120 +++++++++++++++++++++++++++++++++---------- securecookie_test.go | 13 ++++- 2 files changed, 105 insertions(+), 28 deletions(-) diff --git a/securecookie.go b/securecookie.go index b718ce9..9846c02 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,21 @@ 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. + b = append(make([]byte, 8, 8+len(b)+s.compactMacSize()), b...) + binary.BigEndian.PutUint64(b, uint64(s.timestamp())) + 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 +315,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 +355,40 @@ 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 + t1 = int64(binary.BigEndian.Uint64(b)) + // extract payload + b = b[8:] } + // 4. Verify date ranges. t2 := s.timestamp() if s.minAge != 0 && t1 > t2-s.minAge { return errTimestampTooNew @@ -340,10 +397,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 +424,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 +702,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..4dca20c 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)) 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) } From 89a9195fa285beb2bb5a9e0b95f3393082217d4b Mon Sep 17 00:00:00 2001 From: Sokolov Yura Date: Mon, 29 Jun 2020 11:08:25 +0300 Subject: [PATCH 2/2] use varint to encode timestamp in compact encoding --- securecookie.go | 10 ++++++---- securecookie_test.go | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/securecookie.go b/securecookie.go index 9846c02..5f98fb5 100644 --- a/securecookie.go +++ b/securecookie.go @@ -299,8 +299,9 @@ func (s *SecureCookie) Encode(name string, value interface{}) (string, error) { b = append(b, mac...) } else { // 3. Create MAC for concatenation of name, date and value. - b = append(make([]byte, 8, 8+len(b)+s.compactMacSize()), b...) - binary.BigEndian.PutUint64(b, uint64(s.timestamp())) + 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...) @@ -384,9 +385,10 @@ func (s *SecureCookie) Decode(name, value string, dst interface{}) error { return err } // extract timestamp - t1 = int64(binary.BigEndian.Uint64(b)) + var tl int + t1, tl = binary.Varint(b) // extract payload - b = b[8:] + b = b[tl:] } // 4. Verify date ranges. t2 := s.timestamp() diff --git a/securecookie_test.go b/securecookie_test.go index 4dca20c..95bbb01 100644 --- a/securecookie_test.go +++ b/securecookie_test.go @@ -45,7 +45,7 @@ func TestSecureCookie(t *testing.T) { t.Error(err1) continue } - t.Log("i", i, "len", len(encoded)) + t.Log("i", i, "len", len(encoded), "c", encoded) dst := make(map[string]interface{}) err2 := s1.Decode("sid", encoded, &dst) if err2 != nil {