Split non-cipher code to utils.go out of ciphers.go
This commit is contained in:
		
							parent
							
								
									ce2e92bc57
								
							
						
					
					
						commit
						f60e24d9c3
					
				|  | @ -3,112 +3,12 @@ package encryption | |||
| import ( | ||||
| 	"crypto/aes" | ||||
| 	"crypto/cipher" | ||||
| 	"crypto/hmac" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"hash" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
 | ||||
| func SecretBytes(secret string) []byte { | ||||
| 	b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(secret, "=")) | ||||
| 	if err == nil { | ||||
| 		// Only return decoded form if a valid AES length
 | ||||
| 		// Don't want unintentional decoding resulting in invalid lengths confusing a user
 | ||||
| 		// that thought they used a 16, 24, 32 length string
 | ||||
| 		for _, i := range []int{16, 24, 32} { | ||||
| 			if len(b) == i { | ||||
| 				return b | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	// If decoding didn't work or resulted in non-AES compliant length,
 | ||||
| 	// assume the raw string was the intended secret
 | ||||
| 	return []byte(secret) | ||||
| } | ||||
| 
 | ||||
| // cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set.
 | ||||
| // additionally, the 'value' is encrypted so it's opaque to the browser
 | ||||
| 
 | ||||
| // Validate ensures a cookie is properly signed
 | ||||
| func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value []byte, t time.Time, ok bool) { | ||||
| 	// value, timestamp, sig
 | ||||
| 	parts := strings.Split(cookie.Value, "|") | ||||
| 	if len(parts) != 3 { | ||||
| 		return | ||||
| 	} | ||||
| 	if checkSignature(parts[2], seed, cookie.Name, parts[0], parts[1]) { | ||||
| 		ts, err := strconv.Atoi(parts[1]) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 		// The expiration timestamp set when the cookie was created
 | ||||
| 		// isn't sent back by the browser. Hence, we check whether the
 | ||||
| 		// creation timestamp stored in the cookie falls within the
 | ||||
| 		// window defined by (Now()-expiration, Now()].
 | ||||
| 		t = time.Unix(int64(ts), 0) | ||||
| 		if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) { | ||||
| 			// it's a valid cookie. now get the contents
 | ||||
| 			rawValue, err := base64.URLEncoding.DecodeString(parts[0]) | ||||
| 			if err == nil { | ||||
| 				value = rawValue | ||||
| 				ok = true | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // SignedValue returns a cookie that is signed and can later be checked with Validate
 | ||||
| func SignedValue(seed string, key string, value []byte, now time.Time) string { | ||||
| 	encodedValue := base64.URLEncoding.EncodeToString(value) | ||||
| 	timeStr := fmt.Sprintf("%d", now.Unix()) | ||||
| 	sig := cookieSignature(sha256.New, seed, key, encodedValue, timeStr) | ||||
| 	cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig) | ||||
| 	return cookieVal | ||||
| } | ||||
| 
 | ||||
| func cookieSignature(signer func() hash.Hash, args ...string) string { | ||||
| 	h := hmac.New(signer, []byte(args[0])) | ||||
| 	for _, arg := range args[1:] { | ||||
| 		h.Write([]byte(arg)) | ||||
| 	} | ||||
| 	var b []byte | ||||
| 	b = h.Sum(b) | ||||
| 	return base64.URLEncoding.EncodeToString(b) | ||||
| } | ||||
| 
 | ||||
| func checkSignature(signature string, args ...string) bool { | ||||
| 	checkSig := cookieSignature(sha256.New, args...) | ||||
| 	if checkHmac(signature, checkSig) { | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: After appropriate rollout window, remove support for SHA1
 | ||||
| 	legacySig := cookieSignature(sha1.New, args...) | ||||
| 	return checkHmac(signature, legacySig) | ||||
| } | ||||
| 
 | ||||
| func checkHmac(input, expected string) bool { | ||||
| 	inputMAC, err1 := base64.URLEncoding.DecodeString(input) | ||||
| 	if err1 == nil { | ||||
| 		expectedMAC, err2 := base64.URLEncoding.DecodeString(expected) | ||||
| 		if err2 == nil { | ||||
| 			return hmac.Equal(inputMAC, expectedMAC) | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
| // Cipher provides methods to encrypt and decrypt
 | ||||
| type Cipher interface { | ||||
| 	Encrypt(value []byte) ([]byte, error) | ||||
|  |  | |||
|  | @ -2,103 +2,13 @@ package encryption | |||
| 
 | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestSecretBytesEncoded(t *testing.T) { | ||||
| 	for _, secretSize := range []int{16, 24, 32} { | ||||
| 		t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { | ||||
| 			secret := make([]byte, secretSize) | ||||
| 			_, err := io.ReadFull(rand.Reader, secret) | ||||
| 			assert.Equal(t, nil, err) | ||||
| 
 | ||||
| 			// We test both padded & raw Base64 to ensure we handle both
 | ||||
| 			// potential user input routes for Base64
 | ||||
| 			base64Padded := base64.URLEncoding.EncodeToString(secret) | ||||
| 			sb := SecretBytes(base64Padded) | ||||
| 			assert.Equal(t, secret, sb) | ||||
| 			assert.Equal(t, len(sb), secretSize) | ||||
| 
 | ||||
| 			base64Raw := base64.RawURLEncoding.EncodeToString(secret) | ||||
| 			sb = SecretBytes(base64Raw) | ||||
| 			assert.Equal(t, secret, sb) | ||||
| 			assert.Equal(t, len(sb), secretSize) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // A string that isn't intended as Base64 and still decodes (but to unintended length)
 | ||||
| // will return the original secret as bytes
 | ||||
| func TestSecretBytesEncodedWrongSize(t *testing.T) { | ||||
| 	for _, secretSize := range []int{15, 20, 28, 33, 44} { | ||||
| 		t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { | ||||
| 			secret := make([]byte, secretSize) | ||||
| 			_, err := io.ReadFull(rand.Reader, secret) | ||||
| 			assert.Equal(t, nil, err) | ||||
| 
 | ||||
| 			// We test both padded & raw Base64 to ensure we handle both
 | ||||
| 			// potential user input routes for Base64
 | ||||
| 			base64Padded := base64.URLEncoding.EncodeToString(secret) | ||||
| 			sb := SecretBytes(base64Padded) | ||||
| 			assert.NotEqual(t, secret, sb) | ||||
| 			assert.NotEqual(t, len(sb), secretSize) | ||||
| 			// The given secret is returned as []byte
 | ||||
| 			assert.Equal(t, base64Padded, string(sb)) | ||||
| 
 | ||||
| 			base64Raw := base64.RawURLEncoding.EncodeToString(secret) | ||||
| 			sb = SecretBytes(base64Raw) | ||||
| 			assert.NotEqual(t, secret, sb) | ||||
| 			assert.NotEqual(t, len(sb), secretSize) | ||||
| 			// The given secret is returned as []byte
 | ||||
| 			assert.Equal(t, base64Raw, string(sb)) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestSecretBytesNonBase64(t *testing.T) { | ||||
| 	trailer := "equals==========" | ||||
| 	assert.Equal(t, trailer, string(SecretBytes(trailer))) | ||||
| 
 | ||||
| 	raw16 := "asdflkjhqwer)(*&" | ||||
| 	sb16 := SecretBytes(raw16) | ||||
| 	assert.Equal(t, raw16, string(sb16)) | ||||
| 	assert.Equal(t, 16, len(sb16)) | ||||
| 
 | ||||
| 	raw24 := "asdflkjhqwer)(*&CJEN#$%^" | ||||
| 	sb24 := SecretBytes(raw24) | ||||
| 	assert.Equal(t, raw24, string(sb24)) | ||||
| 	assert.Equal(t, 24, len(sb24)) | ||||
| 
 | ||||
| 	raw32 := "asdflkjhqwer)(*&1234lkjhqwer)(*&" | ||||
| 	sb32 := SecretBytes(raw32) | ||||
| 	assert.Equal(t, raw32, string(sb32)) | ||||
| 	assert.Equal(t, 32, len(sb32)) | ||||
| } | ||||
| 
 | ||||
| func TestSignAndValidate(t *testing.T) { | ||||
| 	seed := "0123456789abcdef" | ||||
| 	key := "cookie-name" | ||||
| 	value := base64.URLEncoding.EncodeToString([]byte("I am soooo encoded")) | ||||
| 	epoch := "123456789" | ||||
| 
 | ||||
| 	sha256sig := cookieSignature(sha256.New, seed, key, value, epoch) | ||||
| 	sha1sig := cookieSignature(sha1.New, seed, key, value, epoch) | ||||
| 
 | ||||
| 	assert.True(t, checkSignature(sha256sig, seed, key, value, epoch)) | ||||
| 	// This should be switched to False after fully deprecating SHA1
 | ||||
| 	assert.True(t, checkSignature(sha1sig, seed, key, value, epoch)) | ||||
| 
 | ||||
| 	assert.False(t, checkSignature(sha256sig, seed, key, "tampered", epoch)) | ||||
| 	assert.False(t, checkSignature(sha1sig, seed, key, "tampered", epoch)) | ||||
| } | ||||
| 
 | ||||
| func TestEncodeAndDecodeAccessToken(t *testing.T) { | ||||
| 	const secret = "0123456789abcdefghijklmnopqrstuv" | ||||
| 	const token = "my access token" | ||||
|  |  | |||
|  | @ -0,0 +1,107 @@ | |||
| package encryption | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/hmac" | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"hash" | ||||
| 	"net/http" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
 | ||||
| func SecretBytes(secret string) []byte { | ||||
| 	b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(secret, "=")) | ||||
| 	if err == nil { | ||||
| 		// Only return decoded form if a valid AES length
 | ||||
| 		// Don't want unintentional decoding resulting in invalid lengths confusing a user
 | ||||
| 		// that thought they used a 16, 24, 32 length string
 | ||||
| 		for _, i := range []int{16, 24, 32} { | ||||
| 			if len(b) == i { | ||||
| 				return b | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	// If decoding didn't work or resulted in non-AES compliant length,
 | ||||
| 	// assume the raw string was the intended secret
 | ||||
| 	return []byte(secret) | ||||
| } | ||||
| 
 | ||||
| // cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set.
 | ||||
| // additionally, the 'value' is encrypted so it's opaque to the browser
 | ||||
| 
 | ||||
| // Validate ensures a cookie is properly signed
 | ||||
| func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value []byte, t time.Time, ok bool) { | ||||
| 	// value, timestamp, sig
 | ||||
| 	parts := strings.Split(cookie.Value, "|") | ||||
| 	if len(parts) != 3 { | ||||
| 		return | ||||
| 	} | ||||
| 	if checkSignature(parts[2], seed, cookie.Name, parts[0], parts[1]) { | ||||
| 		ts, err := strconv.Atoi(parts[1]) | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} | ||||
| 		// The expiration timestamp set when the cookie was created
 | ||||
| 		// isn't sent back by the browser. Hence, we check whether the
 | ||||
| 		// creation timestamp stored in the cookie falls within the
 | ||||
| 		// window defined by (Now()-expiration, Now()].
 | ||||
| 		t = time.Unix(int64(ts), 0) | ||||
| 		if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) { | ||||
| 			// it's a valid cookie. now get the contents
 | ||||
| 			rawValue, err := base64.URLEncoding.DecodeString(parts[0]) | ||||
| 			if err == nil { | ||||
| 				value = rawValue | ||||
| 				ok = true | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // SignedValue returns a cookie that is signed and can later be checked with Validate
 | ||||
| func SignedValue(seed string, key string, value []byte, now time.Time) string { | ||||
| 	encodedValue := base64.URLEncoding.EncodeToString(value) | ||||
| 	timeStr := fmt.Sprintf("%d", now.Unix()) | ||||
| 	sig := cookieSignature(sha256.New, seed, key, encodedValue, timeStr) | ||||
| 	cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig) | ||||
| 	return cookieVal | ||||
| } | ||||
| 
 | ||||
| func cookieSignature(signer func() hash.Hash, args ...string) string { | ||||
| 	h := hmac.New(signer, []byte(args[0])) | ||||
| 	for _, arg := range args[1:] { | ||||
| 		h.Write([]byte(arg)) | ||||
| 	} | ||||
| 	var b []byte | ||||
| 	b = h.Sum(b) | ||||
| 	return base64.URLEncoding.EncodeToString(b) | ||||
| } | ||||
| 
 | ||||
| func checkSignature(signature string, args ...string) bool { | ||||
| 	checkSig := cookieSignature(sha256.New, args...) | ||||
| 	if checkHmac(signature, checkSig) { | ||||
| 		return true | ||||
| 	} | ||||
| 
 | ||||
| 	// TODO: After appropriate rollout window, remove support for SHA1
 | ||||
| 	legacySig := cookieSignature(sha1.New, args...) | ||||
| 	return checkHmac(signature, legacySig) | ||||
| } | ||||
| 
 | ||||
| func checkHmac(input, expected string) bool { | ||||
| 	inputMAC, err1 := base64.URLEncoding.DecodeString(input) | ||||
| 	if err1 == nil { | ||||
| 		expectedMAC, err2 := base64.URLEncoding.DecodeString(expected) | ||||
| 		if err2 == nil { | ||||
| 			return hmac.Equal(inputMAC, expectedMAC) | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
| 
 | ||||
|  | @ -0,0 +1,100 @@ | |||
| package encryption | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| 	"crypto/sha1" | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestSecretBytesEncoded(t *testing.T) { | ||||
| 	for _, secretSize := range []int{16, 24, 32} { | ||||
| 		t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { | ||||
| 			secret := make([]byte, secretSize) | ||||
| 			_, err := io.ReadFull(rand.Reader, secret) | ||||
| 			assert.Equal(t, nil, err) | ||||
| 
 | ||||
| 			// We test both padded & raw Base64 to ensure we handle both
 | ||||
| 			// potential user input routes for Base64
 | ||||
| 			base64Padded := base64.URLEncoding.EncodeToString(secret) | ||||
| 			sb := SecretBytes(base64Padded) | ||||
| 			assert.Equal(t, secret, sb) | ||||
| 			assert.Equal(t, len(sb), secretSize) | ||||
| 
 | ||||
| 			base64Raw := base64.RawURLEncoding.EncodeToString(secret) | ||||
| 			sb = SecretBytes(base64Raw) | ||||
| 			assert.Equal(t, secret, sb) | ||||
| 			assert.Equal(t, len(sb), secretSize) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // A string that isn't intended as Base64 and still decodes (but to unintended length)
 | ||||
| // will return the original secret as bytes
 | ||||
| func TestSecretBytesEncodedWrongSize(t *testing.T) { | ||||
| 	for _, secretSize := range []int{15, 20, 28, 33, 44} { | ||||
| 		t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { | ||||
| 			secret := make([]byte, secretSize) | ||||
| 			_, err := io.ReadFull(rand.Reader, secret) | ||||
| 			assert.Equal(t, nil, err) | ||||
| 
 | ||||
| 			// We test both padded & raw Base64 to ensure we handle both
 | ||||
| 			// potential user input routes for Base64
 | ||||
| 			base64Padded := base64.URLEncoding.EncodeToString(secret) | ||||
| 			sb := SecretBytes(base64Padded) | ||||
| 			assert.NotEqual(t, secret, sb) | ||||
| 			assert.NotEqual(t, len(sb), secretSize) | ||||
| 			// The given secret is returned as []byte
 | ||||
| 			assert.Equal(t, base64Padded, string(sb)) | ||||
| 
 | ||||
| 			base64Raw := base64.RawURLEncoding.EncodeToString(secret) | ||||
| 			sb = SecretBytes(base64Raw) | ||||
| 			assert.NotEqual(t, secret, sb) | ||||
| 			assert.NotEqual(t, len(sb), secretSize) | ||||
| 			// The given secret is returned as []byte
 | ||||
| 			assert.Equal(t, base64Raw, string(sb)) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestSecretBytesNonBase64(t *testing.T) { | ||||
| 	trailer := "equals==========" | ||||
| 	assert.Equal(t, trailer, string(SecretBytes(trailer))) | ||||
| 
 | ||||
| 	raw16 := "asdflkjhqwer)(*&" | ||||
| 	sb16 := SecretBytes(raw16) | ||||
| 	assert.Equal(t, raw16, string(sb16)) | ||||
| 	assert.Equal(t, 16, len(sb16)) | ||||
| 
 | ||||
| 	raw24 := "asdflkjhqwer)(*&CJEN#$%^" | ||||
| 	sb24 := SecretBytes(raw24) | ||||
| 	assert.Equal(t, raw24, string(sb24)) | ||||
| 	assert.Equal(t, 24, len(sb24)) | ||||
| 
 | ||||
| 	raw32 := "asdflkjhqwer)(*&1234lkjhqwer)(*&" | ||||
| 	sb32 := SecretBytes(raw32) | ||||
| 	assert.Equal(t, raw32, string(sb32)) | ||||
| 	assert.Equal(t, 32, len(sb32)) | ||||
| } | ||||
| 
 | ||||
| func TestSignAndValidate(t *testing.T) { | ||||
| 	seed := "0123456789abcdef" | ||||
| 	key := "cookie-name" | ||||
| 	value := base64.URLEncoding.EncodeToString([]byte("I am soooo encoded")) | ||||
| 	epoch := "123456789" | ||||
| 
 | ||||
| 	sha256sig := cookieSignature(sha256.New, seed, key, value, epoch) | ||||
| 	sha1sig := cookieSignature(sha1.New, seed, key, value, epoch) | ||||
| 
 | ||||
| 	assert.True(t, checkSignature(sha256sig, seed, key, value, epoch)) | ||||
| 	// This should be switched to False after fully deprecating SHA1
 | ||||
| 	assert.True(t, checkSignature(sha1sig, seed, key, value, epoch)) | ||||
| 
 | ||||
| 	assert.False(t, checkSignature(sha256sig, seed, key, "tampered", epoch)) | ||||
| 	assert.False(t, checkSignature(sha1sig, seed, key, "tampered", epoch)) | ||||
| } | ||||
		Loading…
	
		Reference in New Issue