@@ -5,28 +5,33 @@ import (
5
5
"encoding/base64"
6
6
"encoding/binary"
7
7
"hash"
8
- "sync"
9
8
10
9
"golang.org/x/crypto/blake2s"
11
10
"golang.org/x/crypto/chacha20"
12
11
)
13
12
14
13
const (
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
21
20
)
22
21
23
- func (s * SecureCookie ) prepareCompactKeys () {
22
+ func (s * SecureCookie ) prepareCompact () {
24
23
// initialize for compact encoding even if no genCompact set to allow
25
24
// 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 [:])
28
28
_ , _ = bl .Write (s .blockKey )
29
29
copy (s .compactBlockKey [:], bl .Sum (nil ))
30
+
31
+ s .macPool .New = func () interface {} {
32
+ hsh , _ := blake2s .New128 (s .hashKey [:])
33
+ return & macbuf {Hash : hsh }
34
+ }
30
35
}
31
36
32
37
func (s * SecureCookie ) encodeCompact (name string , serialized []byte ) (string , error ) {
@@ -35,24 +40,25 @@ func (s *SecureCookie) encodeCompact(name string, serialized []byte) (string, er
35
40
}
36
41
37
42
// Check length
38
- encodedLen := base64 .URLEncoding .EncodedLen (len (serialized ) + macLen + timeLen + versionLen )
43
+ encodedLen := base64 .URLEncoding .EncodedLen (len (serialized ) + macLen + headerLen )
39
44
if s .maxLength != 0 && encodedLen > s .maxLength {
40
45
return "" , errEncodedValueTooLong
41
46
}
42
47
43
48
// 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 )
49
+ r := make ([]byte , headerLen + macLen + len (serialized ))
50
+ macHeader , body := r [:macHeaderLen ], r [macHeaderLen :]
51
+ copy (body , serialized )
52
+
53
+ header , mac := macHeader [:headerLen ], macHeader [headerLen :]
54
+ binary .BigEndian .PutUint64 (header , uint64 (timeShift (timestampNano ())))
55
+ header [0 ] = version // it is made free in timestamp
50
56
51
57
// Mac
52
- s .compactMac (version , name , body , tag )
58
+ s .compactMac (header , name , body , mac )
53
59
54
60
// Encrypt (if needed)
55
- s .compactXorStream (tag , body )
61
+ s .compactXorStream (macHeader , body )
56
62
57
63
// Encode
58
64
return base64 .RawURLEncoding .EncodeToString (r ), nil
@@ -68,118 +74,96 @@ func (s *SecureCookie) decodeCompact(name string, encoded string, dest interface
68
74
return cookieError {cause : err , typ : decodeError , msg : "base64 decode failed" }
69
75
}
70
76
71
- if len (encoded ) < macLen + timeLen + versionLen {
77
+ if len (encoded ) < macHeaderLen {
72
78
return errValueToDecodeTooShort
73
79
}
74
80
81
+ macHeader , body := decoded [:macHeaderLen ], decoded [macHeaderLen :]
82
+ header , mac := macHeader [:headerLen ], macHeader [headerLen :]
83
+
75
84
// Decompose
76
- if decoded [0 ] != version {
85
+ if header [0 ] != version {
77
86
// there is only version currently
78
87
return errVersionDoesntMatch
79
88
}
80
89
81
- m := decoded [versionLen :]
82
- tag , body := m [:macLen ], m [macLen :]
83
-
84
- // Decrypt (if need)
85
- s .compactXorStream (tag , body )
86
-
87
90
// Check time
88
- ts := int64 (binary .LittleEndian .Uint64 (body ))
89
- now := timeShift ( timestampNano () )
90
- if s .maxAge > 0 && ts + secsShift (s .maxAge ) < now {
91
+ ts := timeUnshift ( int64 (binary .BigEndian .Uint64 (header ) ))
92
+ now := timestampNano ()
93
+ if s .maxAge > 0 && ts + secs2nano (s .maxAge ) < now {
91
94
return errTimestampExpired
92
95
}
93
- if s .minAge > 0 && ts + secsShift (s .minAge ) > now {
96
+ if s .minAge > 0 && ts + secs2nano (s .minAge ) > now {
94
97
return errTimestampExpired
95
98
}
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
- }
102
99
103
- // Verify
104
- var mac [macLen ]byte
105
- s .compactMac (version , name , body , mac [:])
106
- if subtle .ConstantTimeCompare (mac [:], tag ) == 0 {
100
+ // Decrypt (if need)
101
+ s .compactXorStream (macHeader , body )
102
+
103
+ // Check MAC
104
+ var macCheck [macLen ]byte
105
+ s .compactMac (header , name , body , macCheck [:])
106
+ if subtle .ConstantTimeCompare (mac , macCheck [:]) == 0 {
107
107
return ErrMacInvalid
108
108
}
109
109
110
110
// Deserialize
111
- if err := s .sz .Deserialize (body [ timeLen :] , dest ); err != nil {
111
+ if err := s .sz .Deserialize (body , dest ); err != nil {
112
112
return cookieError {cause : err , typ : decodeError }
113
113
}
114
114
115
115
return nil
116
116
}
117
117
118
- var macPool = sync.Pool {New : func () interface {} {
119
- hsh , _ := blake2s .New256 (nil )
120
- return & macbuf {Hash : hsh }
121
- }}
122
-
123
118
type macbuf struct {
124
119
hash.Hash
125
- buf [3 + nameMaxLen ]byte
126
- sum [32 ]byte
120
+ buf [1 + nameMaxLen ]byte
121
+ sum [16 ]byte
127
122
}
128
123
129
124
func (m * macbuf ) Reset () {
130
125
m .Hash .Reset ()
131
- m .buf = [3 + nameMaxLen ]byte {}
132
- m .sum = [32 ]byte {}
126
+ m .sum = [16 ]byte {}
133
127
}
134
128
135
- func (s * SecureCookie ) compactMac (version byte , name string , body , mac []byte ) {
136
- enc := macPool .Get ().(* macbuf )
129
+ func (s * SecureCookie ) compactMac (header [] byte , name string , body , mac []byte ) {
130
+ enc := s . macPool .Get ().(* macbuf )
137
131
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
132
// 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 )
133
+ enc .buf [0 ] = byte (len (name ))
134
+ copy (enc .buf [1 :], name )
147
135
148
- _ , _ = enc .Write (enc . buf [: 3 + len ( name )] )
149
- _ , _ = enc .Write (s . hashKey [: ])
136
+ _ , _ = enc .Write (header )
137
+ _ , _ = enc .Write (enc . buf [: 1 + len ( name ) ])
150
138
_ , _ = enc .Write (body )
151
139
152
140
copy (mac , enc .Sum (enc .sum [:0 ]))
153
141
154
142
enc .Reset ()
155
- macPool .Put (enc )
143
+ s . macPool .Put (enc )
156
144
}
157
145
158
- func (s * SecureCookie ) compactXorStream (tag , body []byte ) {
146
+ func (s * SecureCookie ) compactXorStream (nonce , body []byte ) {
159
147
if len (s .blockKey ) == 0 { // no blockKey - no encryption
160
148
return
161
149
}
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 ])
150
+ stream , err := chacha20 .NewUnauthenticatedCipher (s .compactBlockKey [:], nonce )
169
151
if err != nil {
170
152
panic ("stream initialization failed" )
171
153
}
172
154
stream .XORKeyStream (body , body )
173
155
}
174
156
157
+ // timeShift ensures high byte is zero to use it for version
175
158
func timeShift (t int64 ) int64 {
176
- return t >> 16
159
+ return t >> 8
177
160
}
178
161
179
- func timeValid (t int64 ) bool {
180
- return (t >> (64 - 16 )) == 0
162
+ // timeUnshift restores timestamp to nanoseconds + clears high byte
163
+ func timeUnshift (t int64 ) int64 {
164
+ return t << 8
181
165
}
182
166
183
- func secsShift (t int64 ) int64 {
184
- return ( t * 1000000000 ) >> 16
167
+ func secs2nano (t int64 ) int64 {
168
+ return t * 1000000000
185
169
}
0 commit comments