Skip to content

Commit f2f5ab0

Browse files
committed
less opinionated scheme
simplify use of Blake2s and ChaCha: - use keyed Blake2s instance - use 192bit nonce with XChaCha20 (add 8 byte 'version+timestamp' header to mac) - get rid of cookie name length restriction (yes, allocate []byte(name))
1 parent f83c210 commit f2f5ab0

File tree

2 files changed

+65
-91
lines changed

2 files changed

+65
-91
lines changed

compact.go

Lines changed: 62 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1413
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
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(s.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

6163
func (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-
123110
type macbuf struct {
124111
hash.Hash
125-
buf [3 + nameMaxLen]byte
126-
sum [32]byte
112+
nameLen [4]byte
113+
sum [16]byte
127114
}
128115

129116
func (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
175148
func 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
}

securecookie.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"io"
2121
"strconv"
2222
"strings"
23+
"sync"
2324
"time"
2425
)
2526

@@ -104,7 +105,6 @@ var (
104105
errDecryptionFailed = cookieError{typ: decodeError, msg: "the value could not be decrypted"}
105106
errValueNotByte = cookieError{typ: decodeError, msg: "value not a []byte."}
106107
errValueNotBytePtr = cookieError{typ: decodeError, msg: "value not a pointer to []byte."}
107-
errNameTooLong = cookieError{typ: usageError, msg: "cookie name is too long"}
108108
errValueToDecodeTooShort = cookieError{typ: decodeError, msg: "the value is too short"}
109109
errVersionDoesntMatch = cookieError{typ: decodeError, msg: "value version unknown"}
110110

@@ -151,7 +151,7 @@ func New(hashKey, blockKey []byte) *SecureCookie {
151151
if blockKey != nil {
152152
s.BlockFunc(aes.NewCipher)
153153
}
154-
s.prepareCompactKeys()
154+
s.prepareCompact()
155155
return s
156156
}
157157

@@ -168,8 +168,8 @@ type SecureCookie struct {
168168
err error
169169
sz Serializer
170170

171-
compactHashKey [keyLen]byte
172171
compactBlockKey [keyLen]byte
172+
macPool sync.Pool
173173
genCompact bool
174174
}
175175

0 commit comments

Comments
 (0)