@@ -5,181 +5,155 @@ import (
55 "encoding/base64"
66 "encoding/binary"
77 "hash"
8- "sync"
98
109 "golang.org/x/crypto/blake2s"
1110 "golang.org/x/crypto/chacha20"
1211)
1312
1413const (
15- nameMaxLen = 127
16- keyLen = 32
17- macLen = 15
18- timeLen = 8
19- versionLen = 1
20- version = 0
14+ nameMaxLen = 127
15+ keyLen = 32
16+ macLen = 16
17+ headerLen = 8
18+ macHeaderLen = macLen + headerLen
19+ version = 1
2120)
2221
23- func (s * SecureCookie ) prepareCompactKeys () {
22+ func (s * SecureCookie ) prepareCompact () {
2423 // initialize for compact encoding even if no genCompact set to allow
2524 // two step migration.
26- s .compactHashKey = blake2s .Sum256 (s .hashKey )
27- bl , _ := blake2s .New256 (s .compactHashKey [:])
25+ hashKey := blake2s .Sum256 (s .hashKey )
26+
27+ bl , _ := blake2s .New256 (hashKey [:])
2828 _ , _ = bl .Write (s .blockKey )
2929 copy (s .compactBlockKey [:], bl .Sum (nil ))
30- }
3130
32- func ( s * SecureCookie ) encodeCompact ( name string , serialized [] byte ) ( string , error ) {
33- if len ( name ) > nameMaxLen {
34- return "" , errNameTooLong
31+ s . macPool . New = func () interface {} {
32+ hsh , _ := blake2s . New128 ( hashKey [:])
33+ return & macbuf { Hash : hsh }
3534 }
35+ }
3636
37+ func (s * SecureCookie ) encodeCompact (name string , serialized []byte ) (string , error ) {
3738 // Check length
38- encodedLen := base64 .URLEncoding .EncodedLen (len (serialized ) + macLen + timeLen + versionLen )
39+ encodedLen := base64 .URLEncoding .EncodedLen (len (serialized ) + macLen + headerLen )
3940 if s .maxLength != 0 && encodedLen > s .maxLength {
4041 return "" , errEncodedValueTooLong
4142 }
4243
4344 // form message
44- r := make ([]byte , versionLen + macLen + timeLen + len (serialized ))
45- r [0 ] = version
46- m := r [versionLen :]
47- tag , body := m [:macLen ], m [macLen :]
48- binary .LittleEndian .PutUint64 (body , uint64 (timeShift (timestampNano ())))
49- copy (body [timeLen :], serialized )
45+ r := make ([]byte , headerLen + macLen + len (serialized ))
46+ macHeader , body := r [:macHeaderLen ], r [macHeaderLen :]
47+ copy (body , serialized )
48+
49+ header , mac := macHeader [:headerLen ], macHeader [headerLen :]
50+ binary .BigEndian .PutUint64 (header , uint64 (timeShift (timestampNano ())))
51+ header [0 ] = version // it is made free in timestamp
5052
5153 // Mac
52- s .compactMac (version , name , body , tag )
54+ s .compactMac (header , name , body , mac )
5355
5456 // Encrypt (if needed)
55- s .compactXorStream (tag , body )
57+ s .compactXorStream (macHeader , body )
5658
5759 // Encode
5860 return base64 .RawURLEncoding .EncodeToString (r ), nil
5961}
6062
6163func (s * SecureCookie ) decodeCompact (name string , encoded string , dest interface {}) error {
62- if len (name ) > nameMaxLen {
63- return errNameTooLong
64- }
65-
6664 decoded , err := base64 .RawURLEncoding .DecodeString (encoded )
6765 if err != nil {
6866 return cookieError {cause : err , typ : decodeError , msg : "base64 decode failed" }
6967 }
7068
71- if len (encoded ) < macLen + timeLen + versionLen {
69+ if len (encoded ) < macHeaderLen {
7270 return errValueToDecodeTooShort
7371 }
7472
73+ macHeader , body := decoded [:macHeaderLen ], decoded [macHeaderLen :]
74+ header , mac := macHeader [:headerLen ], macHeader [headerLen :]
75+
7576 // Decompose
76- if decoded [0 ] != version {
77+ if header [0 ] != version {
7778 // there is only version currently
7879 return errVersionDoesntMatch
7980 }
8081
81- m := decoded [versionLen :]
82- tag , body := m [:macLen ], m [macLen :]
83-
84- // Decrypt (if need)
85- s .compactXorStream (tag , body )
86-
8782 // Check time
88- ts := int64 (binary .LittleEndian .Uint64 (body ))
89- now := timeShift ( timestampNano () )
90- if s .maxAge > 0 && ts + secsShift (s .maxAge ) < now {
83+ ts := timeUnshift ( int64 (binary .BigEndian .Uint64 (header ) ))
84+ now := timestampNano ()
85+ if s .maxAge > 0 && ts + secs2nano (s .maxAge ) < now {
9186 return errTimestampExpired
9287 }
93- if s .minAge > 0 && ts + secsShift (s .minAge ) > now {
88+ if s .minAge > 0 && ts + secs2nano (s .minAge ) > now {
9489 return errTimestampExpired
9590 }
96- if ! timeValid (ts ) {
97- // We are checking bytes we explicitely leaved as zero as preliminary
98- // MAC check. We could do it because ChaCha20 has no known plaintext
99- // issues.
100- return ErrMacInvalid
101- }
10291
103- // Verify
104- var mac [macLen ]byte
105- s .compactMac (version , name , body , mac [:])
106- if subtle .ConstantTimeCompare (mac [:], tag ) == 0 {
92+ // Decrypt (if need)
93+ s .compactXorStream (macHeader , body )
94+
95+ // Check MAC
96+ var macCheck [macLen ]byte
97+ s .compactMac (header , name , body , macCheck [:])
98+ if subtle .ConstantTimeCompare (mac , macCheck [:]) == 0 {
10799 return ErrMacInvalid
108100 }
109101
110102 // Deserialize
111- if err := s .sz .Deserialize (body [ timeLen :] , dest ); err != nil {
103+ if err := s .sz .Deserialize (body , dest ); err != nil {
112104 return cookieError {cause : err , typ : decodeError }
113105 }
114106
115107 return nil
116108}
117109
118- var macPool = sync.Pool {New : func () interface {} {
119- hsh , _ := blake2s .New256 (nil )
120- return & macbuf {Hash : hsh }
121- }}
122-
123110type macbuf struct {
124111 hash.Hash
125- buf [ 3 + nameMaxLen ]byte
126- sum [ 32 ]byte
112+ nameLen [ 4 ]byte
113+ sum [ 16 ]byte
127114}
128115
129116func (m * macbuf ) Reset () {
130117 m .Hash .Reset ()
131- m .buf = [3 + nameMaxLen ]byte {}
132- m .sum = [32 ]byte {}
118+ m .sum = [16 ]byte {}
133119}
134120
135- func (s * SecureCookie ) compactMac (version byte , name string , body , mac []byte ) {
136- enc := macPool .Get ().(* macbuf )
137-
138- // While it is not "recommended" way to mix key in, it is still valid
139- // because 1) Blake2b is not susceptible to length-extention attack, 2)
140- // "recommended" way does almost same, just stores key length in other place
141- // (it mixes length into constan iv itself).
142- enc .buf [0 ] = version
143- // name should not be longer than 127 bytes to fallback to varint in a future
144- enc .buf [1 ] = byte (len (name ))
145- enc .buf [2 ] = keyLen
146- copy (enc .buf [3 :], name )
147-
148- _ , _ = enc .Write (enc .buf [:3 + len (name )])
149- _ , _ = enc .Write (s .hashKey [:])
121+ func (s * SecureCookie ) compactMac (header []byte , name string , body , mac []byte ) {
122+ enc := s .macPool .Get ().(* macbuf )
123+
124+ binary .BigEndian .PutUint32 (enc .nameLen [:], uint32 (len (name )))
125+ _ , _ = enc .Write (header )
126+ _ , _ = enc .Write (enc .nameLen [:])
127+ _ , _ = enc .Write ([]byte (name ))
150128 _ , _ = enc .Write (body )
151129
152130 copy (mac , enc .Sum (enc .sum [:0 ]))
153131
154132 enc .Reset ()
155- macPool .Put (enc )
133+ s . macPool .Put (enc )
156134}
157135
158- func (s * SecureCookie ) compactXorStream (tag , body []byte ) {
136+ func (s * SecureCookie ) compactXorStream (nonce , body []byte ) {
159137 if len (s .blockKey ) == 0 { // no blockKey - no encryption
160138 return
161139 }
162- key := s .compactBlockKey
163- // Mix remaining tag bytes into key.
164- // We may do it because ChaCha20 has no related keys issues.
165- key [29 ] ^= tag [12 ]
166- key [30 ] ^= tag [13 ]
167- key [31 ] ^= tag [14 ]
168- stream , err := chacha20 .NewUnauthenticatedCipher (key [:], tag [:12 ])
140+ stream , err := chacha20 .NewUnauthenticatedCipher (s .compactBlockKey [:], nonce )
169141 if err != nil {
170142 panic ("stream initialization failed" )
171143 }
172144 stream .XORKeyStream (body , body )
173145}
174146
147+ // timeShift ensures high byte is zero to use it for version
175148func timeShift (t int64 ) int64 {
176- return t >> 16
149+ return t >> 8
177150}
178151
179- func timeValid (t int64 ) bool {
180- return (t >> (64 - 16 )) == 0
152+ // timeUnshift restores timestamp to nanoseconds + clears high byte
153+ func timeUnshift (t int64 ) int64 {
154+ return t << 8
181155}
182156
183- func secsShift (t int64 ) int64 {
184- return ( t * 1000000000 ) >> 16
157+ func secs2nano (t int64 ) int64 {
158+ return t * 1000000000
185159}
0 commit comments