git.iko.soy/typeable/oauth2-proxy
on master help sign in
Merge pull request #539 from grnhse/encryption-efficiency-improvements

Encryption efficiency improvements

commita197a17bc38f8583b3c9d588e5656ada9d813e88 authorJoel Speed <Joel.speed@hotmail.co.uk> dateSun 14 Jun 2020, 13:23 (6 years ago) parent1683aa51979627
12 files changed+596−307
CHANGELOG.md+1 pkg/apis/options/sessions.go+3−3 pkg/apis/sessions/session_state.go+31−20 pkg/apis/sessions/session_state_test.go+82−35 pkg/encryption/cipher.go+78−127 pkg/encryption/cipher_test.go+181−108 pkg/encryption/utils.go+106 pkg/encryption/utils_test.go+100 pkg/sessions/cookie/session_store.go+5−5 pkg/sessions/redis/redis_store.go+5−5 pkg/sessions/session_store_test.go+2−2 pkg/validation/options.go+2−2
CHANGELOG.md+1
@@ -55,6 +55,7 @@
5555
5656## Changes since v5.1.1
5757
58+- [#539](https://github.com/oauth2-proxy/oauth2-proxy/pull/539) Refactor encryption ciphers and add AES-GCM support (@NickMeves)
5859- [#601](https://github.com/oauth2-proxy/oauth2-proxy/pull/601) Ensure decrypted user/email are valid UTF8 (@JoelSpeed)
5960- [#560](https://github.com/oauth2-proxy/oauth2-proxy/pull/560) Fallback to UserInfo is User ID claim not present (@JoelSpeed)
6061- [#598](https://github.com/oauth2-proxy/oauth2-proxy/pull/598) acr_values no longer sent to IdP when empty (@ScottGuymer)
pkg/apis/options/sessions.go+3−3
@@ -4,9 +4,9 @@ import "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
44
55// SessionOptions contains configuration options for the SessionStore providers.
66type SessionOptions struct {
7− Type string `flag:"session-store-type" cfg:"session_store_type"`
8− Cipher *encryption.Cipher `cfg:",internal"`
9− Redis RedisStoreOptions `cfg:",squash"`
7+ Type string `flag:"session-store-type" cfg:"session_store_type"`
8+ Cipher encryption.Cipher `cfg:",internal"`
9+ Redis RedisStoreOptions `cfg:",squash"`
1010}
1111
1212// CookieSessionStoreType is used to indicate the CookieSessionStore should be
pkg/apis/sessions/session_state.go+31−20
@@ -60,7 +60,7 @@ func (s *SessionState) String() string {
6060}
6161
6262// EncodeSessionState returns string representation of the current session
63−func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) {
63+func (s *SessionState) EncodeSessionState(c encryption.Cipher) (string, error) {
6464 var ss SessionState
6565 if c == nil {
6666 // Store only Email and User when cipher is unavailable
@@ -77,7 +77,7 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error)
7777 &ss.IDToken,
7878 &ss.RefreshToken,
7979 } {
80− err := c.EncryptInto(s)
80+ err := into(s, c.Encrypt)
8181 if err != nil {
8282 return "", err
8383 }
@@ -89,7 +89,7 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error)
8989}
9090
9191// DecodeSessionState decodes the session cookie string into a SessionState
92−func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
92+func DecodeSessionState(v string, c encryption.Cipher) (*SessionState, error) {
9393 var ss SessionState
9494 err := json.Unmarshal([]byte(v), &ss)
9595 if err != nil {
@@ -104,24 +104,18 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
104104 PreferredUsername: ss.PreferredUsername,
105105 }
106106 } else {
107− // Backward compatibility with using unencrypted Email
108− if ss.Email != "" {
109− decryptedEmail, errEmail := c.Decrypt(ss.Email)
110− if errEmail == nil {
111− if !utf8.ValidString(decryptedEmail) {
112− return nil, errors.New("invalid value for decrypted email")
113− }
114− ss.Email = decryptedEmail
107+ // Backward compatibility with using unencrypted Email or User
108+ // Decryption errors will leave original string
109+ err = into(&ss.Email, c.Decrypt)
110+ if err == nil {
111+ if !utf8.ValidString(ss.Email) {
112+ return nil, errors.New("invalid value for decrypted email")
115113 }
116114 }
117− // Backward compatibility with using unencrypted User
118− if ss.User != "" {
119− decryptedUser, errUser := c.Decrypt(ss.User)
120− if errUser == nil {
121− if !utf8.ValidString(decryptedUser) {
122− return nil, errors.New("invalid value for decrypted user")
123− }
124− ss.User = decryptedUser
115+ err = into(&ss.User, c.Decrypt)
116+ if err == nil {
117+ if !utf8.ValidString(ss.User) {
118+ return nil, errors.New("invalid value for decrypted user")
125119 }
126120 }
127121
@@ -131,7 +125,7 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
131125 &ss.IDToken,
132126 &ss.RefreshToken,
133127 } {
134− err := c.DecryptInto(s)
128+ err := into(s, c.Decrypt)
135129 if err != nil {
136130 return nil, err
137131 }
@@ -139,3 +133,20 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) {
139133 }
140134 return &ss, nil
141135}
136+
137+// codecFunc is a function that takes a []byte and encodes/decodes it
138+type codecFunc func([]byte) ([]byte, error)
139+
140+func into(s *string, f codecFunc) error {
141+ // Do not encrypt/decrypt nil or empty strings
142+ if s == nil || *s == "" {
143+ return nil
144+ }
145+
146+ d, err := f([]byte(*s))
147+ if err != nil {
148+ return err
149+ }
150+ *s = string(d)
151+ return nil
152+}
pkg/apis/sessions/session_state_test.go+82−35
@@ -1,11 +1,13 @@
1−package sessions_test
1+package sessions
22
33import (
4+ "crypto/rand"
45 "fmt"
6+ "io"
7+ mathrand "math/rand"
58 "testing"
69 "time"
710
8− "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions"
911 "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption"
1012 "github.com/stretchr/testify/assert"
1113)
@@ -17,12 +19,16 @@ func timePtr(t time.Time) *time.Time {
1719 return &t
1820}
1921
22+func newTestCipher(secret []byte) (encryption.Cipher, error) {
23+ return encryption.NewBase64Cipher(encryption.NewCFBCipher, secret)
24+}
25+
2026func TestSessionStateSerialization(t *testing.T) {
21− c, err := encryption.NewCipher([]byte(secret))
27+ c, err := newTestCipher([]byte(secret))
2228 assert.Equal(t, nil, err)
23− c2, err := encryption.NewCipher([]byte(altSecret))
29+ c2, err := newTestCipher([]byte(altSecret))
2430 assert.Equal(t, nil, err)
25− s := &sessions.SessionState{
31+ s := &SessionState{
2632 Email: "user@domain.com",
2733 PreferredUsername: "user",
2834 AccessToken: "token1234",
@@ -34,7 +40,7 @@ func TestSessionStateSerialization(t *testing.T) {
3440 encoded, err := s.EncodeSessionState(c)
3541 assert.Equal(t, nil, err)
3642
37− ss, err := sessions.DecodeSessionState(encoded, c)
43+ ss, err := DecodeSessionState(encoded, c)
3844 t.Logf("%#v", ss)
3945 assert.Equal(t, nil, err)
4046 assert.Equal(t, "", ss.User)
@@ -47,17 +53,17 @@ func TestSessionStateSerialization(t *testing.T) {
4753 assert.Equal(t, s.RefreshToken, ss.RefreshToken)
4854
4955 // ensure a different cipher can't decode properly (ie: it gets gibberish)
50− ss, err = sessions.DecodeSessionState(encoded, c2)
56+ ss, err = DecodeSessionState(encoded, c2)
5157 t.Logf("%#v", ss)
5258 assert.NotEqual(t, nil, err)
5359}
5460
5561func TestSessionStateSerializationWithUser(t *testing.T) {
56− c, err := encryption.NewCipher([]byte(secret))
62+ c, err := newTestCipher([]byte(secret))
5763 assert.Equal(t, nil, err)
58− c2, err := encryption.NewCipher([]byte(altSecret))
64+ c2, err := newTestCipher([]byte(altSecret))
5965 assert.Equal(t, nil, err)
60− s := &sessions.SessionState{
66+ s := &SessionState{
6167 User: "just-user",
6268 PreferredUsername: "ju",
6369 Email: "user@domain.com",
@@ -69,7 +75,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
6975 encoded, err := s.EncodeSessionState(c)
7076 assert.Equal(t, nil, err)
7177
72− ss, err := sessions.DecodeSessionState(encoded, c)
78+ ss, err := DecodeSessionState(encoded, c)
7379 t.Logf("%#v", ss)
7480 assert.Equal(t, nil, err)
7581 assert.Equal(t, s.User, ss.User)
@@ -81,13 +87,13 @@ func TestSessionStateSerializationWithUser(t *testing.T) {
8187 assert.Equal(t, s.RefreshToken, ss.RefreshToken)
8288
8389 // ensure a different cipher can't decode properly (ie: it gets gibberish)
84− ss, err = sessions.DecodeSessionState(encoded, c2)
90+ ss, err = DecodeSessionState(encoded, c2)
8591 t.Logf("%#v", ss)
8692 assert.NotEqual(t, nil, err)
8793}
8894
8995func TestSessionStateSerializationNoCipher(t *testing.T) {
90− s := &sessions.SessionState{
96+ s := &SessionState{
9197 Email: "user@domain.com",
9298 PreferredUsername: "user",
9399 AccessToken: "token1234",
@@ -99,7 +105,7 @@ func TestSessionStateSerializationNoCipher(t *testing.T) {
99105 assert.Equal(t, nil, err)
100106
101107 // only email should have been serialized
102− ss, err := sessions.DecodeSessionState(encoded, nil)
108+ ss, err := DecodeSessionState(encoded, nil)
103109 assert.Equal(t, nil, err)
104110 assert.Equal(t, "", ss.User)
105111 assert.Equal(t, s.Email, ss.Email)
@@ -109,7 +115,7 @@ func TestSessionStateSerializationNoCipher(t *testing.T) {
109115}
110116
111117func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
112− s := &sessions.SessionState{
118+ s := &SessionState{
113119 User: "just-user",
114120 Email: "user@domain.com",
115121 PreferredUsername: "user",
@@ -122,7 +128,7 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
122128 assert.Equal(t, nil, err)
123129
124130 // only email should have been serialized
125− ss, err := sessions.DecodeSessionState(encoded, nil)
131+ ss, err := DecodeSessionState(encoded, nil)
126132 assert.Equal(t, nil, err)
127133 assert.Equal(t, s.User, ss.User)
128134 assert.Equal(t, s.Email, ss.Email)
@@ -132,20 +138,20 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) {
132138}
133139
134140func TestExpired(t *testing.T) {
135− s := &sessions.SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(-1) * time.Minute))}
141+ s := &SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(-1) * time.Minute))}
136142 assert.Equal(t, true, s.IsExpired())
137143
138− s = &sessions.SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(1) * time.Minute))}
144+ s = &SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(1) * time.Minute))}
139145 assert.Equal(t, false, s.IsExpired())
140146
141− s = &sessions.SessionState{}
147+ s = &SessionState{}
142148 assert.Equal(t, false, s.IsExpired())
143149}
144150
145151type testCase struct {
146− sessions.SessionState
152+ SessionState
147153 Encoded string
148− Cipher *encryption.Cipher
154+ Cipher encryption.Cipher
149155 Error bool
150156}
151157
@@ -159,14 +165,14 @@ func TestEncodeSessionState(t *testing.T) {
159165
160166 testCases := []testCase{
161167 {
162− SessionState: sessions.SessionState{
168+ SessionState: SessionState{
163169 Email: "user@domain.com",
164170 User: "just-user",
165171 },
166172 Encoded: `{"Email":"user@domain.com","User":"just-user"}`,
167173 },
168174 {
169− SessionState: sessions.SessionState{
175+ SessionState: SessionState{
170176 Email: "user@domain.com",
171177 User: "just-user",
172178 AccessToken: "token1234",
@@ -181,7 +187,7 @@ func TestEncodeSessionState(t *testing.T) {
181187
182188 for i, tc := range testCases {
183189 encoded, err := tc.EncodeSessionState(tc.Cipher)
184− t.Logf("i:%d Encoded:%#vsessions.SessionState:%#v Error:%#v", i, encoded, tc.SessionState, err)
190+ t.Logf("i:%d Encoded:%#vSessionState:%#v Error:%#v", i, encoded, tc.SessionState, err)
185191 if tc.Error {
186192 assert.Error(t, err)
187193 assert.Empty(t, encoded)
@@ -201,39 +207,39 @@ func TestDecodeSessionState(t *testing.T) {
201207 eJSON, _ := e.MarshalJSON()
202208 eString := string(eJSON)
203209
204− c, err := encryption.NewCipher([]byte(secret))
210+ c, err := newTestCipher([]byte(secret))
205211 assert.NoError(t, err)
206212
207213 testCases := []testCase{
208214 {
209− SessionState: sessions.SessionState{
215+ SessionState: SessionState{
210216 Email: "user@domain.com",
211217 User: "just-user",
212218 },
213219 Encoded: `{"Email":"user@domain.com","User":"just-user"}`,
214220 },
215221 {
216− SessionState: sessions.SessionState{
222+ SessionState: SessionState{
217223 Email: "user@domain.com",
218224 User: "",
219225 },
220226 Encoded: `{"Email":"user@domain.com"}`,
221227 },
222228 {
223− SessionState: sessions.SessionState{
229+ SessionState: SessionState{
224230 User: "just-user",
225231 },
226232 Encoded: `{"User":"just-user"}`,
227233 },
228234 {
229− SessionState: sessions.SessionState{
235+ SessionState: SessionState{
230236 Email: "user@domain.com",
231237 User: "just-user",
232238 },
233239 Encoded: fmt.Sprintf(`{"Email":"user@domain.com","User":"just-user","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString),
234240 },
235241 {
236− SessionState: sessions.SessionState{
242+ SessionState: SessionState{
237243 Email: "user@domain.com",
238244 User: "just-user",
239245 AccessToken: "token1234",
@@ -246,7 +252,7 @@ func TestDecodeSessionState(t *testing.T) {
246252 Cipher: c,
247253 },
248254 {
249− SessionState: sessions.SessionState{
255+ SessionState: SessionState{
250256 Email: "user@domain.com",
251257 User: "just-user",
252258 },
@@ -264,7 +270,7 @@ func TestDecodeSessionState(t *testing.T) {
264270 Error: true,
265271 },
266272 {
267− SessionState: sessions.SessionState{
273+ SessionState: SessionState{
268274 Email: "user@domain.com",
269275 User: "YmFzZTY0LWVuY29kZWQtdXNlcgo=", // Base64 encoding of base64-encoded-user
270276 },
@@ -274,8 +280,8 @@ func TestDecodeSessionState(t *testing.T) {
274280 }
275281
276282 for i, tc := range testCases {
277− ss, err := sessions.DecodeSessionState(tc.Encoded, tc.Cipher)
278− t.Logf("i:%d Encoded:%#vsessions.SessionState:%#v Error:%#v", i, tc.Encoded, ss, err)
283+ ss, err := DecodeSessionState(tc.Encoded, tc.Cipher)
284+ t.Logf("i:%d Encoded:%#vSessionState:%#v Error:%#v", i, tc.Encoded, ss, err)
279285 if tc.Error {
280286 assert.Error(t, err)
281287 assert.Nil(t, ss)
@@ -297,7 +303,7 @@ func TestDecodeSessionState(t *testing.T) {
297303}
298304
299305func TestSessionStateAge(t *testing.T) {
300− ss := &sessions.SessionState{}
306+ ss := &SessionState{}
301307
302308 // Created at unset so should be 0
303309 assert.Equal(t, time.Duration(0), ss.Age())
@@ -306,3 +312,44 @@ func TestSessionStateAge(t *testing.T) {
306312 ss.CreatedAt = timePtr(time.Now().Add(-1 * time.Hour))
307313 assert.Equal(t, time.Hour, ss.Age().Round(time.Minute))
308314}
315+
316+func TestIntoEncryptAndIntoDecrypt(t *testing.T) {
317+ const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
318+
319+ // Test all 3 valid AES sizes
320+ for _, secretSize := range []int{16, 24, 32} {
321+ t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
322+ secret := make([]byte, secretSize)
323+ _, err := io.ReadFull(rand.Reader, secret)
324+ assert.Equal(t, nil, err)
325+
326+ c, err := newTestCipher(secret)
327+ assert.NoError(t, err)
328+
329+ // Check no errors with empty or nil strings
330+ empty := ""
331+ assert.Equal(t, nil, into(&empty, c.Encrypt))
332+ assert.Equal(t, nil, into(&empty, c.Decrypt))
333+ assert.Equal(t, nil, into(nil, c.Encrypt))
334+ assert.Equal(t, nil, into(nil, c.Decrypt))
335+
336+ // Test various sizes tokens might be
337+ for _, dataSize := range []int{10, 100, 1000, 5000, 10000} {
338+ t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) {
339+ b := make([]byte, dataSize)
340+ for i := range b {
341+ b[i] = charset[mathrand.Intn(len(charset))]
342+ }
343+ data := string(b)
344+ originalData := data
345+
346+ assert.Equal(t, nil, into(&data, c.Encrypt))
347+ assert.NotEqual(t, originalData, data)
348+
349+ assert.Equal(t, nil, into(&data, c.Decrypt))
350+ assert.Equal(t, originalData, data)
351+ })
352+ }
353+ })
354+ }
355+}
pkg/encryption/cipher.go+78−127
@@ -3,183 +3,134 @@ package encryption
33import (
44 "crypto/aes"
55 "crypto/cipher"
6− "crypto/hmac"
76 "crypto/rand"
8− "crypto/sha1"
9− "crypto/sha256"
107 "encoding/base64"
118 "fmt"
12− "hash"
139 "io"
14− "net/http"
15− "strconv"
16− "strings"
17− "time"
1810)
1911
20−// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
21−func SecretBytes(secret string) []byte {
22− b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(secret, "="))
23− if err == nil {
24− // Only return decoded form if a valid AES length
25− // Don't want unintentional decoding resulting in invalid lengths confusing a user
26− // that thought they used a 16, 24, 32 length string
27− for _, i := range []int{16, 24, 32} {
28− if len(b) == i {
29− return b
30− }
31− }
32− }
33− // If decoding didn't work or resulted in non-AES compliant length,
34− // assume the raw string was the intended secret
35− return []byte(secret)
36−}
37−
38−// cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set.
39−// additionally, the 'value' is encrypted so it's opaque to the browser
40−
41−// Validate ensures a cookie is properly signed
42−func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value string, t time.Time, ok bool) {
43− // value, timestamp, sig
44− parts := strings.Split(cookie.Value, "|")
45− if len(parts) != 3 {
46− return
47− }
48− if checkSignature(parts[2], seed, cookie.Name, parts[0], parts[1]) {
49− ts, err := strconv.Atoi(parts[1])
50− if err != nil {
51− return
52− }
53− // The expiration timestamp set when the cookie was created
54− // isn't sent back by the browser. Hence, we check whether the
55− // creation timestamp stored in the cookie falls within the
56− // window defined by (Now()-expiration, Now()].
57− t = time.Unix(int64(ts), 0)
58− if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) {
59− // it's a valid cookie. now get the contents
60− rawValue, err := base64.URLEncoding.DecodeString(parts[0])
61− if err == nil {
62− value = string(rawValue)
63− ok = true
64− return
65− }
66− }
67− }
68− return
12+// Cipher provides methods to encrypt and decrypt
13+type Cipher interface {
14+ Encrypt(value []byte) ([]byte, error)
15+ Decrypt(ciphertext []byte) ([]byte, error)
6916}
7017
71−// SignedValue returns a cookie that is signed and can later be checked with Validate
72−func SignedValue(seed string, key string, value string, now time.Time) string {
73− encodedValue := base64.URLEncoding.EncodeToString([]byte(value))
74− timeStr := fmt.Sprintf("%d", now.Unix())
75− sig := cookieSignature(sha256.New, seed, key, encodedValue, timeStr)
76− cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig)
77− return cookieVal
18+type base64Cipher struct {
19+ Cipher Cipher
7820}
7921
80−func cookieSignature(signer func() hash.Hash, args ...string) string {
81− h := hmac.New(signer, []byte(args[0]))
82− for _, arg := range args[1:] {
83− h.Write([]byte(arg))
22+// NewBase64Cipher returns a new AES Cipher for encrypting cookie values
23+// and wrapping them in Base64 -- Supports Legacy encryption scheme
24+func NewBase64Cipher(initCipher func([]byte) (Cipher, error), secret []byte) (Cipher, error) {
25+ c, err := initCipher(secret)
26+ if err != nil {
27+ return nil, err
8428 }
85− var b []byte
86− b = h.Sum(b)
87− return base64.URLEncoding.EncodeToString(b)
29+ return &base64Cipher{Cipher: c}, nil
8830}
8931
90−func checkSignature(signature string, args ...string) bool {
91− checkSig := cookieSignature(sha256.New, args...)
92− if checkHmac(signature, checkSig) {
93− return true
32+// Encrypt encrypts a value with the embedded Cipher & Base64 encodes it
33+func (c *base64Cipher) Encrypt(value []byte) ([]byte, error) {
34+ encrypted, err := c.Cipher.Encrypt(value)
35+ if err != nil {
36+ return nil, err
9437 }
9538
96− // TODO: After appropriate rollout window, remove support for SHA1
97− legacySig := cookieSignature(sha1.New, args...)
98− return checkHmac(signature, legacySig)
39+ return []byte(base64.StdEncoding.EncodeToString(encrypted)), nil
9940}
10041
101−func checkHmac(input, expected string) bool {
102− inputMAC, err1 := base64.URLEncoding.DecodeString(input)
103− if err1 == nil {
104− expectedMAC, err2 := base64.URLEncoding.DecodeString(expected)
105− if err2 == nil {
106− return hmac.Equal(inputMAC, expectedMAC)
107− }
42+// Decrypt Base64 decodes a value & decrypts it with the embedded Cipher
43+func (c *base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) {
44+ encrypted, err := base64.StdEncoding.DecodeString(string(ciphertext))
45+ if err != nil {
46+ return nil, fmt.Errorf("failed to base64 decode value %s", err)
10847 }
109− return false
48+
49+ return c.Cipher.Decrypt(encrypted)
11050}
11151
112−// Cipher provides methods to encrypt and decrypt cookie values
113−type Cipher struct {
52+type cfbCipher struct {
11453 cipher.Block
11554}
11655
117−// NewCipher returns a new aes Cipher for encrypting cookie values
118−func NewCipher(secret []byte) (*Cipher, error) {
56+// NewCFBCipher returns a new AES CFB Cipher
57+func NewCFBCipher(secret []byte) (Cipher, error) {
11958 c, err := aes.NewCipher(secret)
12059 if err != nil {
12160 return nil, err
12261 }
123− return &Cipher{Block: c}, err
62+ return &cfbCipher{Block: c}, err
12463}
12564
126−// Encrypt a value for use in a cookie
127−func (c *Cipher) Encrypt(value string) (string, error) {
65+// Encrypt with AES CFB
66+func (c *cfbCipher) Encrypt(value []byte) ([]byte, error) {
12867 ciphertext := make([]byte, aes.BlockSize+len(value))
12968 iv := ciphertext[:aes.BlockSize]
13069 if _, err := io.ReadFull(rand.Reader, iv); err != nil {
131− return "", fmt.Errorf("failed to create initialization vector %s", err)
70+ return nil, fmt.Errorf("failed to create initialization vector %s", err)
13271 }
13372
13473 stream := cipher.NewCFBEncrypter(c.Block, iv)
135− stream.XORKeyStream(ciphertext[aes.BlockSize:], []byte(value))
136− return base64.StdEncoding.EncodeToString(ciphertext), nil
74+ stream.XORKeyStream(ciphertext[aes.BlockSize:], value)
75+ return ciphertext, nil
13776}
13877
139−// Decrypt a value from a cookie to it's original string
140−func (c *Cipher) Decrypt(s string) (string, error) {
141− encrypted, err := base64.StdEncoding.DecodeString(s)
142− if err != nil {
143− return "", fmt.Errorf("failed to decrypt cookie value %s", err)
144− }
145−
146− if len(encrypted) < aes.BlockSize {
147− return "", fmt.Errorf("encrypted cookie value should be "+
148− "at least %d bytes, but is only %d bytes",
149− aes.BlockSize, len(encrypted))
78+// Decrypt an AES CFB ciphertext
79+func (c *cfbCipher) Decrypt(ciphertext []byte) ([]byte, error) {
80+ if len(ciphertext) < aes.BlockSize {
81+ return nil, fmt.Errorf("encrypted value should be at least %d bytes, but is only %d bytes", aes.BlockSize, len(ciphertext))
15082 }
15183
152− iv := encrypted[:aes.BlockSize]
153− encrypted = encrypted[aes.BlockSize:]
84+ iv, ciphertext := ciphertext[:aes.BlockSize], ciphertext[aes.BlockSize:]
85+ plaintext := make([]byte, len(ciphertext))
15486 stream := cipher.NewCFBDecrypter(c.Block, iv)
155− stream.XORKeyStream(encrypted, encrypted)
87+ stream.XORKeyStream(plaintext, ciphertext)
15688
157− return string(encrypted), nil
89+ return plaintext, nil
15890}
15991
160−// EncryptInto encrypts the value and stores it back in the string pointer
161−func (c *Cipher) EncryptInto(s *string) error {
162− return into(c.Encrypt, s)
92+type gcmCipher struct {
93+ cipher.Block
16394}
16495
165−// DecryptInto decrypts the value and stores it back in the string pointer
166−func (c *Cipher) DecryptInto(s *string) error {
167− return into(c.Decrypt, s)
96+// NewGCMCipher returns a new AES GCM Cipher
97+func NewGCMCipher(secret []byte) (Cipher, error) {
98+ c, err := aes.NewCipher(secret)
99+ if err != nil {
100+ return nil, err
101+ }
102+ return &gcmCipher{Block: c}, err
168103}
169104
170−// codecFunc is a function that takes a string and encodes/decodes it
171−type codecFunc func(string) (string, error)
105+// Encrypt with AES GCM on raw bytes
106+func (c *gcmCipher) Encrypt(value []byte) ([]byte, error) {
107+ gcm, err := cipher.NewGCM(c.Block)
108+ if err != nil {
109+ return nil, err
110+ }
111+ nonce := make([]byte, gcm.NonceSize())
112+ if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
113+ return nil, err
114+ }
115+ // Using nonce as Seal's dst argument results in it being the first
116+ // chunk of bytes in the ciphertext. Decrypt retrieves the nonce/IV from this.
117+ ciphertext := gcm.Seal(nonce, nonce, value, nil)
118+ return ciphertext, nil
119+}
172120
173−func into(f codecFunc, s *string) error {
174− // Do not encrypt/decrypt nil or empty strings
175− if s == nil || *s == "" {
176− return nil
121+// Decrypt an AES GCM ciphertext
122+func (c *gcmCipher) Decrypt(ciphertext []byte) ([]byte, error) {
123+ gcm, err := cipher.NewGCM(c.Block)
124+ if err != nil {
125+ return nil, err
177126 }
178127
179− d, err := f(*s)
128+ nonceSize := gcm.NonceSize()
129+ nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
130+
131+ plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
180132 if err != nil {
181− return err
133+ return nil, err
182134 }
183− *s = d
184− return nil
135+ return plaintext, nil
185136}
pkg/encryption/cipher_test.go+181−108
@@ -2,8 +2,6 @@ package encryption
22
33import (
44 "crypto/rand"
5− "crypto/sha1"
6− "crypto/sha256"
75 "encoding/base64"
86 "fmt"
97 "io"
@@ -12,146 +10,221 @@ import (
1210 "github.com/stretchr/testify/assert"
1311)
1412
15−func TestSecretBytesEncoded(t *testing.T) {
16− for _, secretSize := range []int{16, 24, 32} {
17− t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
18− secret := make([]byte, secretSize)
19− _, err := io.ReadFull(rand.Reader, secret)
20− assert.Equal(t, nil, err)
13+func TestEncodeAndDecodeAccessToken(t *testing.T) {
14+ const secret = "0123456789abcdefghijklmnopqrstuv"
15+ const token = "my access token"
16+ c, err := NewBase64Cipher(NewCFBCipher, []byte(secret))
17+ assert.Equal(t, nil, err)
2118
22− // We test both padded & raw Base64 to ensure we handle both
23− // potential user input routes for Base64
24− base64Padded := base64.URLEncoding.EncodeToString(secret)
25− sb := SecretBytes(base64Padded)
26− assert.Equal(t, secret, sb)
27− assert.Equal(t, len(sb), secretSize)
28−
29− base64Raw := base64.RawURLEncoding.EncodeToString(secret)
30− sb = SecretBytes(base64Raw)
31− assert.Equal(t, secret, sb)
32− assert.Equal(t, len(sb), secretSize)
33− })
34− }
35−}
19+ encoded, err := c.Encrypt([]byte(token))
20+ assert.Equal(t, nil, err)
3621
37−// A string that isn't intended as Base64 and still decodes (but to unintended length)
38−// will return the original secret as bytes
39−func TestSecretBytesEncodedWrongSize(t *testing.T) {
40− for _, secretSize := range []int{15, 20, 28, 33, 44} {
41− t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
42− secret := make([]byte, secretSize)
43− _, err := io.ReadFull(rand.Reader, secret)
44− assert.Equal(t, nil, err)
22+ decoded, err := c.Decrypt(encoded)
23+ assert.Equal(t, nil, err)
4524
46− // We test both padded & raw Base64 to ensure we handle both
47− // potential user input routes for Base64
48− base64Padded := base64.URLEncoding.EncodeToString(secret)
49− sb := SecretBytes(base64Padded)
50− assert.NotEqual(t, secret, sb)
51− assert.NotEqual(t, len(sb), secretSize)
52− // The given secret is returned as []byte
53− assert.Equal(t, base64Padded, string(sb))
54−
55− base64Raw := base64.RawURLEncoding.EncodeToString(secret)
56− sb = SecretBytes(base64Raw)
57− assert.NotEqual(t, secret, sb)
58− assert.NotEqual(t, len(sb), secretSize)
59− // The given secret is returned as []byte
60− assert.Equal(t, base64Raw, string(sb))
61− })
62− }
25+ assert.NotEqual(t, []byte(token), encoded)
26+ assert.Equal(t, []byte(token), decoded)
6327}
6428
65−func TestSecretBytesNonBase64(t *testing.T) {
66− trailer := "equals=========="
67− assert.Equal(t, trailer, string(SecretBytes(trailer)))
29+func TestEncodeAndDecodeAccessTokenB64(t *testing.T) {
30+ const secretBase64 = "A3Xbr6fu6Al0HkgrP1ztjb-mYiwmxgNPP-XbNsz1WBk="
31+ const token = "my access token"
6832
69− raw16 := "asdflkjhqwer)(*&"
70− sb16 := SecretBytes(raw16)
71− assert.Equal(t, raw16, string(sb16))
72− assert.Equal(t, 16, len(sb16))
33+ secret, err := base64.URLEncoding.DecodeString(secretBase64)
34+ assert.Equal(t, nil, err)
35+ c, err := NewBase64Cipher(NewCFBCipher, []byte(secret))
36+ assert.Equal(t, nil, err)
7337
74− raw24 := "asdflkjhqwer)(*&CJEN#$%^"
75− sb24 := SecretBytes(raw24)
76− assert.Equal(t, raw24, string(sb24))
77− assert.Equal(t, 24, len(sb24))
38+ encoded, err := c.Encrypt([]byte(token))
39+ assert.Equal(t, nil, err)
7840
79− raw32 := "asdflkjhqwer)(*&1234lkjhqwer)(*&"
80− sb32 := SecretBytes(raw32)
81− assert.Equal(t, raw32, string(sb32))
82− assert.Equal(t, 32, len(sb32))
41+ decoded, err := c.Decrypt(encoded)
42+ assert.Equal(t, nil, err)
43+
44+ assert.NotEqual(t, []byte(token), encoded)
45+ assert.Equal(t, []byte(token), decoded)
8346}
8447
85−func TestSignAndValidate(t *testing.T) {
86− seed := "0123456789abcdef"
87− key := "cookie-name"
88− value := base64.URLEncoding.EncodeToString([]byte("I am soooo encoded"))
89− epoch := "123456789"
48+func TestEncryptAndDecrypt(t *testing.T) {
49+ // Test our 2 cipher types
50+ cipherInits := map[string]func([]byte) (Cipher, error){
51+ "CFB": NewCFBCipher,
52+ "GCM": NewGCMCipher,
53+ }
54+ for name, initCipher := range cipherInits {
55+ t.Run(name, func(t *testing.T) {
56+ // Test all 3 valid AES sizes
57+ for _, secretSize := range []int{16, 24, 32} {
58+ t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
59+ secret := make([]byte, secretSize)
60+ _, err := io.ReadFull(rand.Reader, secret)
61+ assert.Equal(t, nil, err)
62+
63+ // Test Standard & Base64 wrapped
64+ cstd, err := initCipher(secret)
65+ assert.Equal(t, nil, err)
66+
67+ cb64, err := NewBase64Cipher(initCipher, secret)
68+ assert.Equal(t, nil, err)
69+
70+ ciphers := map[string]Cipher{
71+ "Standard": cstd,
72+ "Base64": cb64,
73+ }
74+
75+ for cName, c := range ciphers {
76+ t.Run(cName, func(t *testing.T) {
77+ // Test various sizes sessions might be
78+ for _, dataSize := range []int{10, 100, 1000, 5000, 10000} {
79+ t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) {
80+ runEncryptAndDecrypt(t, c, dataSize)
81+ })
82+ }
83+ })
84+ }
85+ })
86+ }
87+ })
88+ }
89+}
9090
91− sha256sig := cookieSignature(sha256.New, seed, key, value, epoch)
92− sha1sig := cookieSignature(sha1.New, seed, key, value, epoch)
91+func runEncryptAndDecrypt(t *testing.T, c Cipher, dataSize int) {
92+ data := make([]byte, dataSize)
93+ _, err := io.ReadFull(rand.Reader, data)
94+ assert.Equal(t, nil, err)
95+
96+ // Ensure our Encrypt function doesn't encrypt in place
97+ immutableData := make([]byte, len(data))
98+ copy(immutableData, data)
99+
100+ encrypted, err := c.Encrypt(data)
101+ assert.Equal(t, nil, err)
102+ assert.NotEqual(t, encrypted, data)
103+ // Encrypt didn't operate in-place on []byte
104+ assert.Equal(t, data, immutableData)
93105
94− assert.True(t, checkSignature(sha256sig, seed, key, value, epoch))
95− // This should be switched to False after fully deprecating SHA1
96− assert.True(t, checkSignature(sha1sig, seed, key, value, epoch))
106+ // Ensure our Decrypt function doesn't decrypt in place
107+ immutableEnc := make([]byte, len(encrypted))
108+ copy(immutableEnc, encrypted)
97109
98− assert.False(t, checkSignature(sha256sig, seed, key, "tampered", epoch))
99− assert.False(t, checkSignature(sha1sig, seed, key, "tampered", epoch))
110+ decrypted, err := c.Decrypt(encrypted)
111+ assert.Equal(t, nil, err)
112+ // Original data back
113+ assert.Equal(t, data, decrypted)
114+ // Decrypt didn't operate in-place on []byte
115+ assert.Equal(t, encrypted, immutableEnc)
116+ // Encrypt/Decrypt actually did something
117+ assert.NotEqual(t, encrypted, decrypted)
100118}
101119
102−func TestEncodeAndDecodeAccessToken(t *testing.T) {
103− const secret = "0123456789abcdefghijklmnopqrstuv"
104− const token = "my access token"
105− c, err := NewCipher([]byte(secret))
120+func TestDecryptCFBWrongSecret(t *testing.T) {
121+ secret1 := []byte("0123456789abcdefghijklmnopqrstuv")
122+ secret2 := []byte("9876543210abcdefghijklmnopqrstuv")
123+
124+ c1, err := NewCFBCipher(secret1)
106125 assert.Equal(t, nil, err)
107126
108− encoded, err := c.Encrypt(token)
127+ c2, err := NewCFBCipher(secret2)
109128 assert.Equal(t, nil, err)
110129
111− decoded, err := c.Decrypt(encoded)
130+ data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df")
131+
132+ ciphertext, err := c1.Encrypt(data)
112133 assert.Equal(t, nil, err)
113134
114− assert.NotEqual(t, token, encoded)
115− assert.Equal(t, token, decoded)
135+ wrongData, err := c2.Decrypt(ciphertext)
136+ assert.Equal(t, nil, err)
137+ assert.NotEqual(t, data, wrongData)
116138}
117139
118−func TestEncodeAndDecodeAccessTokenB64(t *testing.T) {
119− const secretBase64 = "A3Xbr6fu6Al0HkgrP1ztjb-mYiwmxgNPP-XbNsz1WBk="
120− const token = "my access token"
140+func TestDecryptGCMWrongSecret(t *testing.T) {
141+ secret1 := []byte("0123456789abcdefghijklmnopqrstuv")
142+ secret2 := []byte("9876543210abcdefghijklmnopqrstuv")
121143
122− secret, err := base64.URLEncoding.DecodeString(secretBase64)
123− assert.Equal(t, nil, err)
124− c, err := NewCipher([]byte(secret))
144+ c1, err := NewGCMCipher(secret1)
125145 assert.Equal(t, nil, err)
126146
127− encoded, err := c.Encrypt(token)
147+ c2, err := NewGCMCipher(secret2)
128148 assert.Equal(t, nil, err)
129149
130− decoded, err := c.Decrypt(encoded)
150+ data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df")
151+
152+ ciphertext, err := c1.Encrypt(data)
131153 assert.Equal(t, nil, err)
132154
133− assert.NotEqual(t, token, encoded)
134− assert.Equal(t, token, decoded)
155+ // GCM is authenticated - this should lead to message authentication failed
156+ _, err = c2.Decrypt(ciphertext)
157+ assert.Error(t, err)
135158}
136159
137−func TestEncodeIntoAndDecodeIntoAccessToken(t *testing.T) {
138− const secret = "0123456789abcdefghijklmnopqrstuv"
139− c, err := NewCipher([]byte(secret))
140− assert.Equal(t, nil, err)
160+// Encrypt with GCM, Decrypt with CFB: Results in Garbage data
161+func TestGCMtoCFBErrors(t *testing.T) {
162+ // Test all 3 valid AES sizes
163+ for _, secretSize := range []int{16, 24, 32} {
164+ t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
165+ secret := make([]byte, secretSize)
166+ _, err := io.ReadFull(rand.Reader, secret)
167+ assert.Equal(t, nil, err)
141168
142− token := "my access token"
143− originalToken := token
169+ gcm, err := NewGCMCipher(secret)
170+ assert.Equal(t, nil, err)
144171
145− assert.Equal(t, nil, c.EncryptInto(&token))
146− assert.NotEqual(t, originalToken, token)
172+ cfb, err := NewCFBCipher(secret)
173+ assert.Equal(t, nil, err)
147174
148− assert.Equal(t, nil, c.DecryptInto(&token))
149− assert.Equal(t, originalToken, token)
175+ // Test various sizes sessions might be
176+ for _, dataSize := range []int{10, 100, 1000, 5000, 10000} {
177+ t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) {
178+ data := make([]byte, dataSize)
179+ _, err := io.ReadFull(rand.Reader, data)
180+ assert.Equal(t, nil, err)
181+
182+ encrypted, err := gcm.Encrypt(data)
183+ assert.Equal(t, nil, err)
184+ assert.NotEqual(t, encrypted, data)
185+
186+ decrypted, err := cfb.Decrypt(encrypted)
187+ assert.Equal(t, nil, err)
188+ // Data is mangled
189+ assert.NotEqual(t, data, decrypted)
190+ assert.NotEqual(t, encrypted, decrypted)
191+ })
192+ }
193+ })
194+ }
195+}
150196
151− // Check no errors with empty or nil strings
152− empty := ""
153− assert.Equal(t, nil, c.EncryptInto(&empty))
154− assert.Equal(t, nil, c.DecryptInto(&empty))
155− assert.Equal(t, nil, c.EncryptInto(nil))
156− assert.Equal(t, nil, c.DecryptInto(nil))
197+// Encrypt with CFB, Decrypt with GCM: Results in errors
198+func TestCFBtoGCMErrors(t *testing.T) {
199+ // Test all 3 valid AES sizes
200+ for _, secretSize := range []int{16, 24, 32} {
201+ t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
202+ secret := make([]byte, secretSize)
203+ _, err := io.ReadFull(rand.Reader, secret)
204+ assert.Equal(t, nil, err)
205+
206+ gcm, err := NewGCMCipher(secret)
207+ assert.Equal(t, nil, err)
208+
209+ cfb, err := NewCFBCipher(secret)
210+ assert.Equal(t, nil, err)
211+
212+ // Test various sizes sessions might be
213+ for _, dataSize := range []int{10, 100, 1000, 5000, 10000} {
214+ t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) {
215+ data := make([]byte, dataSize)
216+ _, err := io.ReadFull(rand.Reader, data)
217+ assert.Equal(t, nil, err)
218+
219+ encrypted, err := cfb.Encrypt(data)
220+ assert.Equal(t, nil, err)
221+ assert.NotEqual(t, encrypted, data)
222+
223+ // GCM is authenticated - this should lead to message authentication failed
224+ _, err = gcm.Decrypt(encrypted)
225+ assert.Error(t, err)
226+ })
227+ }
228+ })
229+ }
157230}
pkg/encryption/utils.gonew file+106
@@ -0,0 +1,106 @@
1+package encryption
2+
3+import (
4+ "crypto/hmac"
5+ "crypto/sha1"
6+ "crypto/sha256"
7+ "encoding/base64"
8+ "fmt"
9+ "hash"
10+ "net/http"
11+ "strconv"
12+ "strings"
13+ "time"
14+)
15+
16+// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
17+func SecretBytes(secret string) []byte {
18+ b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(secret, "="))
19+ if err == nil {
20+ // Only return decoded form if a valid AES length
21+ // Don't want unintentional decoding resulting in invalid lengths confusing a user
22+ // that thought they used a 16, 24, 32 length string
23+ for _, i := range []int{16, 24, 32} {
24+ if len(b) == i {
25+ return b
26+ }
27+ }
28+ }
29+ // If decoding didn't work or resulted in non-AES compliant length,
30+ // assume the raw string was the intended secret
31+ return []byte(secret)
32+}
33+
34+// cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set.
35+// additionally, the 'value' is encrypted so it's opaque to the browser
36+
37+// Validate ensures a cookie is properly signed
38+func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value []byte, t time.Time, ok bool) {
39+ // value, timestamp, sig
40+ parts := strings.Split(cookie.Value, "|")
41+ if len(parts) != 3 {
42+ return
43+ }
44+ if checkSignature(parts[2], seed, cookie.Name, parts[0], parts[1]) {
45+ ts, err := strconv.Atoi(parts[1])
46+ if err != nil {
47+ return
48+ }
49+ // The expiration timestamp set when the cookie was created
50+ // isn't sent back by the browser. Hence, we check whether the
51+ // creation timestamp stored in the cookie falls within the
52+ // window defined by (Now()-expiration, Now()].
53+ t = time.Unix(int64(ts), 0)
54+ if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) {
55+ // it's a valid cookie. now get the contents
56+ rawValue, err := base64.URLEncoding.DecodeString(parts[0])
57+ if err == nil {
58+ value = rawValue
59+ ok = true
60+ return
61+ }
62+ }
63+ }
64+ return
65+}
66+
67+// SignedValue returns a cookie that is signed and can later be checked with Validate
68+func SignedValue(seed string, key string, value []byte, now time.Time) string {
69+ encodedValue := base64.URLEncoding.EncodeToString(value)
70+ timeStr := fmt.Sprintf("%d", now.Unix())
71+ sig := cookieSignature(sha256.New, seed, key, encodedValue, timeStr)
72+ cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig)
73+ return cookieVal
74+}
75+
76+func cookieSignature(signer func() hash.Hash, args ...string) string {
77+ h := hmac.New(signer, []byte(args[0]))
78+ for _, arg := range args[1:] {
79+ h.Write([]byte(arg))
80+ }
81+ var b []byte
82+ b = h.Sum(b)
83+ return base64.URLEncoding.EncodeToString(b)
84+}
85+
86+func checkSignature(signature string, args ...string) bool {
87+ checkSig := cookieSignature(sha256.New, args...)
88+ if checkHmac(signature, checkSig) {
89+ return true
90+ }
91+
92+ // TODO: After appropriate rollout window, remove support for SHA1
93+ legacySig := cookieSignature(sha1.New, args...)
94+ return checkHmac(signature, legacySig)
95+}
96+
97+func checkHmac(input, expected string) bool {
98+ inputMAC, err1 := base64.URLEncoding.DecodeString(input)
99+ if err1 == nil {
100+ expectedMAC, err2 := base64.URLEncoding.DecodeString(expected)
101+ if err2 == nil {
102+ return hmac.Equal(inputMAC, expectedMAC)
103+ }
104+ }
105+ return false
106+}
pkg/encryption/utils_test.gonew file+100
@@ -0,0 +1,100 @@
1+package encryption
2+
3+import (
4+ "crypto/rand"
5+ "crypto/sha1"
6+ "crypto/sha256"
7+ "encoding/base64"
8+ "fmt"
9+ "io"
10+ "testing"
11+
12+ "github.com/stretchr/testify/assert"
13+)
14+
15+func TestSecretBytesEncoded(t *testing.T) {
16+ for _, secretSize := range []int{16, 24, 32} {
17+ t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
18+ secret := make([]byte, secretSize)
19+ _, err := io.ReadFull(rand.Reader, secret)
20+ assert.Equal(t, nil, err)
21+
22+ // We test both padded & raw Base64 to ensure we handle both
23+ // potential user input routes for Base64
24+ base64Padded := base64.URLEncoding.EncodeToString(secret)
25+ sb := SecretBytes(base64Padded)
26+ assert.Equal(t, secret, sb)
27+ assert.Equal(t, len(sb), secretSize)
28+
29+ base64Raw := base64.RawURLEncoding.EncodeToString(secret)
30+ sb = SecretBytes(base64Raw)
31+ assert.Equal(t, secret, sb)
32+ assert.Equal(t, len(sb), secretSize)
33+ })
34+ }
35+}
36+
37+// A string that isn't intended as Base64 and still decodes (but to unintended length)
38+// will return the original secret as bytes
39+func TestSecretBytesEncodedWrongSize(t *testing.T) {
40+ for _, secretSize := range []int{15, 20, 28, 33, 44} {
41+ t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
42+ secret := make([]byte, secretSize)
43+ _, err := io.ReadFull(rand.Reader, secret)
44+ assert.Equal(t, nil, err)
45+
46+ // We test both padded & raw Base64 to ensure we handle both
47+ // potential user input routes for Base64
48+ base64Padded := base64.URLEncoding.EncodeToString(secret)
49+ sb := SecretBytes(base64Padded)
50+ assert.NotEqual(t, secret, sb)
51+ assert.NotEqual(t, len(sb), secretSize)
52+ // The given secret is returned as []byte
53+ assert.Equal(t, base64Padded, string(sb))
54+
55+ base64Raw := base64.RawURLEncoding.EncodeToString(secret)
56+ sb = SecretBytes(base64Raw)
57+ assert.NotEqual(t, secret, sb)
58+ assert.NotEqual(t, len(sb), secretSize)
59+ // The given secret is returned as []byte
60+ assert.Equal(t, base64Raw, string(sb))
61+ })
62+ }
63+}
64+
65+func TestSecretBytesNonBase64(t *testing.T) {
66+ trailer := "equals=========="
67+ assert.Equal(t, trailer, string(SecretBytes(trailer)))
68+
69+ raw16 := "asdflkjhqwer)(*&"
70+ sb16 := SecretBytes(raw16)
71+ assert.Equal(t, raw16, string(sb16))
72+ assert.Equal(t, 16, len(sb16))
73+
74+ raw24 := "asdflkjhqwer)(*&CJEN#$%^"
75+ sb24 := SecretBytes(raw24)
76+ assert.Equal(t, raw24, string(sb24))
77+ assert.Equal(t, 24, len(sb24))
78+
79+ raw32 := "asdflkjhqwer)(*&1234lkjhqwer)(*&"
80+ sb32 := SecretBytes(raw32)
81+ assert.Equal(t, raw32, string(sb32))
82+ assert.Equal(t, 32, len(sb32))
83+}
84+
85+func TestSignAndValidate(t *testing.T) {
86+ seed := "0123456789abcdef"
87+ key := "cookie-name"
88+ value := base64.URLEncoding.EncodeToString([]byte("I am soooo encoded"))
89+ epoch := "123456789"
90+
91+ sha256sig := cookieSignature(sha256.New, seed, key, value, epoch)
92+ sha1sig := cookieSignature(sha1.New, seed, key, value, epoch)
93+
94+ assert.True(t, checkSignature(sha256sig, seed, key, value, epoch))
95+ // This should be switched to False after fully deprecating SHA1
96+ assert.True(t, checkSignature(sha1sig, seed, key, value, epoch))
97+
98+ assert.False(t, checkSignature(sha256sig, seed, key, "tampered", epoch))
99+ assert.False(t, checkSignature(sha1sig, seed, key, "tampered", epoch))
100+}
pkg/sessions/cookie/session_store.go+5−5
@@ -28,7 +28,7 @@ var _ sessions.SessionStore = &SessionStore{}
2828// interface that stores sessions in client side cookies
2929type SessionStore struct {
3030 CookieOptions *options.CookieOptions
31− CookieCipher *encryption.Cipher
31+ CookieCipher encryption.Cipher
3232}
3333
3434// Save takes a sessions.SessionState and stores the information from it
@@ -59,7 +59,7 @@ func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) {
5959 return nil, errors.New("cookie signature not valid")
6060 }
6161
62− session, err := sessionFromCookie(val, s.CookieCipher)
62+ session, err := sessionFromCookie(string(val), s.CookieCipher)
6363 if err != nil {
6464 return nil, err
6565 }
@@ -84,12 +84,12 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error {
8484}
8585
8686// cookieForSession serializes a session state for storage in a cookie
87−func cookieForSession(s *sessions.SessionState, c *encryption.Cipher) (string, error) {
87+func cookieForSession(s *sessions.SessionState, c encryption.Cipher) (string, error) {
8888 return s.EncodeSessionState(c)
8989}
9090
9191// sessionFromCookie deserializes a session from a cookie value
92−func sessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) {
92+func sessionFromCookie(v string, c encryption.Cipher) (s *sessions.SessionState, err error) {
9393 return sessions.DecodeSessionState(v, c)
9494}
9595
@@ -104,7 +104,7 @@ func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Reques
104104// authentication details
105105func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie {
106106 if value != "" {
107− value = encryption.SignedValue(s.CookieOptions.Secret, s.CookieOptions.Name, value, now)
107+ value = encryption.SignedValue(s.CookieOptions.Secret, s.CookieOptions.Name, []byte(value), now)
108108 }
109109 c := s.makeCookie(req, s.CookieOptions.Name, value, s.CookieOptions.Expire, now)
110110 if len(c.Value) > 4096-len(s.CookieOptions.Name) {
pkg/sessions/redis/redis_store.go+5−5
@@ -32,7 +32,7 @@ type TicketData struct {
3232// SessionStore is an implementation of the sessions.SessionStore
3333// interface that stores sessions in redis
3434type SessionStore struct {
35− CookieCipher *encryption.Cipher
35+ CookieCipher encryption.Cipher
3636 CookieOptions *options.CookieOptions
3737 Client Client
3838}
@@ -175,7 +175,7 @@ func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, erro
175175 return nil, fmt.Errorf("cookie signature not valid")
176176 }
177177 ctx := req.Context()
178− session, err := store.loadSessionFromString(ctx, val)
178+ session, err := store.loadSessionFromString(ctx, string(val))
179179 if err != nil {
180180 return nil, fmt.Errorf("error loading session: %s", err)
181181 }
@@ -237,7 +237,7 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
237237
238238 // We only return an error if we had an issue with redis
239239 // If there's an issue decoding the ticket, ignore it
240− ticket, _ := decodeTicket(store.CookieOptions.Name, val)
240+ ticket, _ := decodeTicket(store.CookieOptions.Name, string(val))
241241 if ticket != nil {
242242 ctx := req.Context()
243243 err := store.Client.Del(ctx, ticket.asHandle(store.CookieOptions.Name))
@@ -251,7 +251,7 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro
251251// makeCookie makes a cookie, signing the value if present
252252func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie {
253253 if value != "" {
254− value = encryption.SignedValue(store.CookieOptions.Secret, store.CookieOptions.Name, value, now)
254+ value = encryption.SignedValue(store.CookieOptions.Secret, store.CookieOptions.Name, []byte(value), now)
255255 }
256256 return cookies.MakeCookieFromOptions(
257257 req,
@@ -302,7 +302,7 @@ func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, e
302302 }
303303
304304 // Valid cookie, decode the ticket
305− ticket, err := decodeTicket(store.CookieOptions.Name, val)
305+ ticket, err := decodeTicket(store.CookieOptions.Name, string(val))
306306 if err != nil {
307307 // If we can't decode the ticket we have to create a new one
308308 return newTicket()
pkg/sessions/session_store_test.go+2−2
@@ -170,7 +170,7 @@ var _ = Describe("NewSessionStore", func() {
170170 BeforeEach(func() {
171171 By("Using a valid cookie with a different providers session encoding")
172172 broken := "BrokenSessionFromADifferentSessionImplementation"
173− value := encryption.SignedValue(cookieOpts.Secret, cookieOpts.Name, broken, time.Now())
173+ value := encryption.SignedValue(cookieOpts.Secret, cookieOpts.Name, []byte(broken), time.Now())
174174 cookie := cookiesapi.MakeCookieFromOptions(request, cookieOpts.Name, value, cookieOpts, cookieOpts.Expire, time.Now())
175175 request.AddCookie(cookie)
176176
@@ -367,7 +367,7 @@ var _ = Describe("NewSessionStore", func() {
367367 _, err := rand.Read(secret)
368368 Expect(err).ToNot(HaveOccurred())
369369 cookieOpts.Secret = base64.URLEncoding.EncodeToString(secret)
370− cipher, err := encryption.NewCipher(encryption.SecretBytes(cookieOpts.Secret))
370+ cipher, err := encryption.NewBase64Cipher(encryption.NewCFBCipher, encryption.SecretBytes(cookieOpts.Secret))
371371 Expect(err).ToNot(HaveOccurred())
372372 Expect(cipher).ToNot(BeNil())
373373 opts.Cipher = cipher
pkg/validation/options.go+2−2
@@ -38,7 +38,7 @@ func Validate(o *options.Options) error {
3838
3939 msgs := make([]string, 0)
4040
41− var cipher *encryption.Cipher
41+ var cipher encryption.Cipher
4242 if o.Cookie.Secret == "" {
4343 msgs = append(msgs, "missing setting: cookie-secret")
4444 } else {
@@ -62,7 +62,7 @@ func Validate(o *options.Options) error {
6262 len(encryption.SecretBytes(o.Cookie.Secret)), suffix))
6363 } else {
6464 var err error
65− cipher, err = encryption.NewCipher(encryption.SecretBytes(o.Cookie.Secret))
65+ cipher, err = encryption.NewBase64Cipher(encryption.NewCFBCipher, encryption.SecretBytes(o.Cookie.Secret))
6666 if err != nil {
6767 msgs = append(msgs, fmt.Sprintf("cookie-secret error: %v", err))
6868 }