Skip to content

Commit 54074fe

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)
1 parent f83c210 commit 54074fe

File tree

2 files changed

+64
-79
lines changed

2 files changed

+64
-79
lines changed

compact.go

Lines changed: 61 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,33 @@ 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+
31+
s.macPool.New = func() interface{} {
32+
hsh, _ := blake2s.New128(s.hashKey[:])
33+
return &macbuf{Hash: hsh}
34+
}
3035
}
3136

3237
func (s *SecureCookie) encodeCompact(name string, serialized []byte) (string, error) {
@@ -35,24 +40,25 @@ func (s *SecureCookie) encodeCompact(name string, serialized []byte) (string, er
3540
}
3641

3742
// Check length
38-
encodedLen := base64.URLEncoding.EncodedLen(len(serialized) + macLen + timeLen + versionLen)
43+
encodedLen := base64.URLEncoding.EncodedLen(len(serialized) + macLen + headerLen)
3944
if s.maxLength != 0 && encodedLen > s.maxLength {
4045
return "", errEncodedValueTooLong
4146
}
4247

4348
// 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
5056

5157
// Mac
52-
s.compactMac(version, name, body, tag)
58+
s.compactMac(header, name, body, mac)
5359

5460
// Encrypt (if needed)
55-
s.compactXorStream(tag, body)
61+
s.compactXorStream(macHeader, body)
5662

5763
// Encode
5864
return base64.RawURLEncoding.EncodeToString(r), nil
@@ -68,118 +74,96 @@ func (s *SecureCookie) decodeCompact(name string, encoded string, dest interface
6874
return cookieError{cause: err, typ: decodeError, msg: "base64 decode failed"}
6975
}
7076

71-
if len(encoded) < macLen+timeLen+versionLen {
77+
if len(encoded) < macHeaderLen {
7278
return errValueToDecodeTooShort
7379
}
7480

81+
macHeader, body := decoded[:macHeaderLen], decoded[macHeaderLen:]
82+
header, mac := macHeader[:headerLen], macHeader[headerLen:]
83+
7584
// Decompose
76-
if decoded[0] != version {
85+
if header[0] != version {
7786
// there is only version currently
7887
return errVersionDoesntMatch
7988
}
8089

81-
m := decoded[versionLen:]
82-
tag, body := m[:macLen], m[macLen:]
83-
84-
// Decrypt (if need)
85-
s.compactXorStream(tag, body)
86-
8790
// 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 {
9194
return errTimestampExpired
9295
}
93-
if s.minAge > 0 && ts+secsShift(s.minAge) > now {
96+
if s.minAge > 0 && ts+secs2nano(s.minAge) > now {
9497
return errTimestampExpired
9598
}
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-
}
10299

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 {
107107
return ErrMacInvalid
108108
}
109109

110110
// Deserialize
111-
if err := s.sz.Deserialize(body[timeLen:], dest); err != nil {
111+
if err := s.sz.Deserialize(body, dest); err != nil {
112112
return cookieError{cause: err, typ: decodeError}
113113
}
114114

115115
return nil
116116
}
117117

118-
var macPool = sync.Pool{New: func() interface{} {
119-
hsh, _ := blake2s.New256(nil)
120-
return &macbuf{Hash: hsh}
121-
}}
122-
123118
type macbuf struct {
124119
hash.Hash
125-
buf [3 + nameMaxLen]byte
126-
sum [32]byte
120+
buf [1 + nameMaxLen]byte
121+
sum [16]byte
127122
}
128123

129124
func (m *macbuf) Reset() {
130125
m.Hash.Reset()
131-
m.buf = [3 + nameMaxLen]byte{}
132-
m.sum = [32]byte{}
126+
m.sum = [16]byte{}
133127
}
134128

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)
137131

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
143132
// 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)
147135

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)])
150138
_, _ = enc.Write(body)
151139

152140
copy(mac, enc.Sum(enc.sum[:0]))
153141

154142
enc.Reset()
155-
macPool.Put(enc)
143+
s.macPool.Put(enc)
156144
}
157145

158-
func (s *SecureCookie) compactXorStream(tag, body []byte) {
146+
func (s *SecureCookie) compactXorStream(nonce, body []byte) {
159147
if len(s.blockKey) == 0 { // no blockKey - no encryption
160148
return
161149
}
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)
169151
if err != nil {
170152
panic("stream initialization failed")
171153
}
172154
stream.XORKeyStream(body, body)
173155
}
174156

157+
// timeShift ensures high byte is zero to use it for version
175158
func timeShift(t int64) int64 {
176-
return t >> 16
159+
return t >> 8
177160
}
178161

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
181165
}
182166

183-
func secsShift(t int64) int64 {
184-
return (t * 1000000000) >> 16
167+
func secs2nano(t int64) int64 {
168+
return t * 1000000000
185169
}

securecookie.go

Lines changed: 3 additions & 2 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

@@ -151,7 +152,7 @@ func New(hashKey, blockKey []byte) *SecureCookie {
151152
if blockKey != nil {
152153
s.BlockFunc(aes.NewCipher)
153154
}
154-
s.prepareCompactKeys()
155+
s.prepareCompact()
155156
return s
156157
}
157158

@@ -168,8 +169,8 @@ type SecureCookie struct {
168169
err error
169170
sz Serializer
170171

171-
compactHashKey [keyLen]byte
172172
compactBlockKey [keyLen]byte
173+
macPool sync.Pool
173174
genCompact bool
174175
}
175176

0 commit comments

Comments
 (0)