git.iko.soy/typeable/oauth2-proxy
on master help sign in
Split non-cipher code to utils.go out of ciphers.go
commitf60e24d9c3582de0eccafd008541ae9a8c386aac authorNick Meves <nick.meves@greenhouse.io> dateSun 10 May 2020, 09:48 (6 years ago) parentce2e92b
4 files changed+207−190
pkg/encryption/cipher.go−100 pkg/encryption/cipher_test.go−90 pkg/encryption/utils.go+107 pkg/encryption/utils_test.go+100
pkg/encryption/cipher.go−100
@@ -3,112 +3,12 @@ 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 []byte, 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 = rawValue
63− ok = true
64− return
65− }
66− }
67− }
68− return
69−}
70−
71−// SignedValue returns a cookie that is signed and can later be checked with Validate
72−func SignedValue(seed string, key string, value []byte, now time.Time) string {
73− encodedValue := base64.URLEncoding.EncodeToString(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
78−}
79−
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))
84− }
85− var b []byte
86− b = h.Sum(b)
87− return base64.URLEncoding.EncodeToString(b)
88−}
89−
90−func checkSignature(signature string, args ...string) bool {
91− checkSig := cookieSignature(sha256.New, args...)
92− if checkHmac(signature, checkSig) {
93− return true
94− }
95−
96− // TODO: After appropriate rollout window, remove support for SHA1
97− legacySig := cookieSignature(sha1.New, args...)
98− return checkHmac(signature, legacySig)
99−}
100−
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− }
108− }
109− return false
110−}
111−
11212// Cipher provides methods to encrypt and decrypt
11313type Cipher interface {
11414 Encrypt(value []byte) ([]byte, error)
pkg/encryption/cipher_test.go−90
@@ -2,103 +2,13 @@ package encryption
22
33import (
44 "crypto/rand"
5− "crypto/sha1"
6− "crypto/sha256"
75 "encoding/base64"
8− "fmt"
96 "io"
107 "testing"
118
129 "github.com/stretchr/testify/assert"
1310)
1411
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−}
101−
10212func TestEncodeAndDecodeAccessToken(t *testing.T) {
10313 const secret = "0123456789abcdefghijklmnopqrstuv"
10414 const token = "my access token"
pkg/encryption/utils.gonew file+107
@@ -0,0 +1,107 @@
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+}
107+
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+}