1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2024-11-28 09:08:44 +02:00

Add GCM Cipher support

During the upcoming encoded session refactor, AES GCM is ideal
to use as the Redis (and other DB like stores) encryption wrapper
around the session because each session is encrypted with a
distinct secret that is passed by the session ticket.
This commit is contained in:
Nick Meves 2020-05-09 17:34:32 -07:00
parent f7cca1d0b3
commit b6931aa4ea
No known key found for this signature in database
GPG Key ID: 93BA8A3CEDCDD1CF
2 changed files with 165 additions and 82 deletions

View File

@ -117,9 +117,27 @@ type Cipher interface {
DecryptInto(s *string) error
}
type DefaultCipher struct {}
// Encrypt is a dummy method for CommonCipher.EncryptInto support
func (c *DefaultCipher) Encrypt(value []byte) ([]byte, error) { return value, nil }
// Decrypt is a dummy method for CommonCipher.DecryptInto support
func (c *DefaultCipher) Decrypt(ciphertext []byte) ([]byte, error) { return ciphertext, nil }
// EncryptInto encrypts the value and stores it back in the string pointer
func (c *DefaultCipher) EncryptInto(s *string) error {
return into(c.Encrypt, s)
}
// DecryptInto decrypts the value and stores it back in the string pointer
func (c *DefaultCipher) DecryptInto(s *string) error {
return into(c.Decrypt, s)
}
// NewCipher returns a new aes Cipher for encrypting cookie values
// This defaults to the Base64 Cipher to align with legacy Encrypt/Decrypt functionality
func NewCipher(secret []byte) (*Base64Cipher, error) {
func NewCipher(secret []byte) (Cipher, error) {
cfb, err := NewCFBCipher(secret)
if err != nil {
return nil, err
@ -128,12 +146,13 @@ func NewCipher(secret []byte) (*Base64Cipher, error) {
}
type Base64Cipher struct {
DefaultCipher
Cipher Cipher
}
// NewBase64Cipher returns a new AES CFB Cipher for encrypting cookie values
// And wrapping them in Base64 -- Supports Legacy encryption scheme
func NewBase64Cipher(c Cipher) (*Base64Cipher, error) {
// NewBase64Cipher returns a new AES Cipher for encrypting cookie values
// and wrapping them in Base64 -- Supports Legacy encryption scheme
func NewBase64Cipher(c Cipher) (Cipher, error) {
return &Base64Cipher{Cipher: c}, nil
}
@ -157,22 +176,13 @@ func (c *Base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) {
return c.Cipher.Decrypt(encrypted)
}
// EncryptInto encrypts the value and stores it back in the string pointer
func (c *Base64Cipher) EncryptInto(s *string) error {
return into(c.Encrypt, s)
}
// DecryptInto decrypts the value and stores it back in the string pointer
func (c *Base64Cipher) DecryptInto(s *string) error {
return into(c.Decrypt, s)
}
type CFBCipher struct {
DefaultCipher
cipher.Block
}
// NewCFBCipher returns a new AES CFB Cipher
func NewCFBCipher(secret []byte) (*CFBCipher, error) {
func NewCFBCipher(secret []byte) (Cipher, error) {
c, err := aes.NewCipher(secret)
if err != nil {
return nil, err
@ -193,7 +203,7 @@ func (c *CFBCipher) Encrypt(value []byte) ([]byte, error) {
return ciphertext, nil
}
// Decrypt a AES CFB ciphertext
// Decrypt an AES CFB ciphertext
func (c *CFBCipher) Decrypt(ciphertext []byte) ([]byte, error) {
if len(ciphertext) < aes.BlockSize {
return nil, fmt.Errorf("encrypted value should be "+
@ -209,20 +219,54 @@ func (c *CFBCipher) Decrypt(ciphertext []byte) ([]byte, error) {
return ciphertext, nil
}
// EncryptInto encrypts the value and stores it back in the string pointer
func (c *CFBCipher) EncryptInto(s *string) error {
return into(c.Encrypt, s)
type GCMCipher struct {
DefaultCipher
cipher.Block
}
// DecryptInto decrypts the value and stores it back in the string pointer
func (c *CFBCipher) DecryptInto(s *string) error {
return into(c.Decrypt, s)
// NewGCMCipher returns a new AES GCM Cipher
func NewGCMCipher(secret []byte) (Cipher, error) {
c, err := aes.NewCipher(secret)
if err != nil {
return nil, err
}
return &GCMCipher{Block: c}, err
}
// Encrypt with AES GCM on raw bytes
func (c *GCMCipher) Encrypt(value []byte) ([]byte, error) {
gcm, err := cipher.NewGCM(c.Block)
if err != nil {
return nil, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
ciphertext := gcm.Seal(nonce, nonce, value, nil)
return ciphertext, nil
}
// Decrypt an AES GCM ciphertext
func (c *GCMCipher) Decrypt(ciphertext []byte) ([]byte, error) {
gcm, err := cipher.NewGCM(c.Block)
if err != nil {
return nil, err
}
nonceSize := gcm.NonceSize()
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
// codecFunc is a function that takes a string and encodes/decodes it
type codecFunc func([]byte) ([]byte, error)
func into(f codecFunc, s *string) error {
// Do not encrypt/decrypt nil or empty strings
if s == nil || *s == "" {

View File

@ -134,17 +134,96 @@ func TestEncodeAndDecodeAccessTokenB64(t *testing.T) {
assert.Equal(t, []byte(token), decoded)
}
func TestEncryptAndDecryptBase64(t *testing.T) {
func TestEncryptAndDecrypt(t *testing.T) {
var err error
// Test our 3 cipher types
for _, initCipher := range []func([]byte) (Cipher, error){NewCipher, NewCFBCipher, NewGCMCipher} {
// Test all 3 valid AES sizes
for _, secretSize := range []int{16, 24, 32} {
secret := make([]byte, secretSize)
_, err = io.ReadFull(rand.Reader, secret)
assert.Equal(t, nil, err)
c, err := initCipher(secret)
assert.Equal(t, nil, err)
// Test various sizes sessions might be
for _, dataSize := range []int{10, 100, 1000, 5000, 10000} {
data := make([]byte, dataSize)
_, err := io.ReadFull(rand.Reader, data)
assert.Equal(t, nil, err)
encrypted, err := c.Encrypt(data)
assert.Equal(t, nil, err)
assert.NotEqual(t, encrypted, data)
decrypted, err := c.Decrypt(encrypted)
assert.Equal(t, nil, err)
assert.Equal(t, data, decrypted)
assert.NotEqual(t, encrypted, decrypted)
}
}
}
}
func TestDecryptWrongSecret(t *testing.T) {
secret1 := []byte("0123456789abcdefghijklmnopqrstuv")
secret2 := []byte("9876543210abcdefghijklmnopqrstuv")
// Test CFB & Base64 (GCM is authenticated, it errors differently)
for _, initCipher := range []func([]byte) (Cipher, error){NewCipher, NewCFBCipher} {
c1, err := initCipher(secret1)
assert.Equal(t, nil, err)
c2, err := initCipher(secret2)
assert.Equal(t, nil, err)
data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df")
ciphertext, err := c1.Encrypt(data)
assert.Equal(t, nil, err)
wrongData, err := c2.Decrypt(ciphertext)
assert.Equal(t, nil, err)
assert.NotEqual(t, data, wrongData)
}
}
func TestDecryptGCMWrongSecret(t *testing.T) {
secret1 := []byte("0123456789abcdefghijklmnopqrstuv")
secret2 := []byte("9876543210abcdefghijklmnopqrstuv")
c1, err := NewGCMCipher(secret1)
assert.Equal(t, nil, err)
c2, err := NewGCMCipher(secret2)
assert.Equal(t, nil, err)
data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df")
ciphertext, err := c1.Encrypt(data)
assert.Equal(t, nil, err)
// GCM is authenticated - this should lead to message authentication failed
_, err = c2.Decrypt(ciphertext)
assert.Error(t, err)
}
func TestIntermixCiphersErrors(t *testing.T) {
var err error
// Encrypt with GCM, Decrypt with CFB: Results in Garbage data
// Test all 3 valid AES sizes
for _, secretSize := range []int{16, 24, 32} {
secret := make([]byte, secretSize)
_, err = io.ReadFull(rand.Reader, secret)
assert.Equal(t, nil, err)
// NewCipher creates a Base64 wrapper of CFBCipher
c, err := NewCipher(secret)
gcm, err := NewGCMCipher(secret)
assert.Equal(t, nil, err)
cfb, err := NewCFBCipher(secret)
assert.Equal(t, nil, err)
// Test various sizes sessions might be
@ -153,48 +232,29 @@ func TestEncryptAndDecryptBase64(t *testing.T) {
_, err := io.ReadFull(rand.Reader, data)
assert.Equal(t, nil, err)
encrypted, err := c.Encrypt(data)
encrypted, err := gcm.Encrypt(data)
assert.Equal(t, nil, err)
assert.NotEqual(t, encrypted, data)
decrypted, err := c.Decrypt(encrypted)
decrypted, err := cfb.Decrypt(encrypted)
assert.Equal(t, nil, err)
assert.Equal(t, data, decrypted)
// Data is mangled
assert.NotEqual(t, data, decrypted)
assert.NotEqual(t, encrypted, decrypted)
}
}
}
func TestDecryptBase64WrongSecret(t *testing.T) {
var err error
secret1 := []byte("0123456789abcdefghijklmnopqrstuv")
secret2 := []byte("9876543210abcdefghijklmnopqrstuv")
c1, err := NewCipher(secret1)
assert.Equal(t, nil, err)
c2, err := NewCipher(secret2)
assert.Equal(t, nil, err)
data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df")
ciphertext, err := c1.Encrypt(data)
assert.Equal(t, nil, err)
wrongData, err := c2.Decrypt(ciphertext)
assert.Equal(t, nil, err)
assert.NotEqual(t, data, wrongData)
}
func TestEncryptAndDecryptCFB(t *testing.T) {
var err error
// Encrypt with CFB, Decrypt with GCM: Results in errors
// Test all 3 valid AES sizes
for _, secretSize := range []int{16, 24, 32} {
secret := make([]byte, secretSize)
_, err = io.ReadFull(rand.Reader, secret)
assert.Equal(t, nil, err)
c, err := NewCFBCipher(secret)
gcm, err := NewGCMCipher(secret)
assert.Equal(t, nil, err)
cfb, err := NewCFBCipher(secret)
assert.Equal(t, nil, err)
// Test various sizes sessions might be
@ -203,38 +263,17 @@ func TestEncryptAndDecryptCFB(t *testing.T) {
_, err := io.ReadFull(rand.Reader, data)
assert.Equal(t, nil, err)
encrypted, err := c.Encrypt(data)
encrypted, err := cfb.Encrypt(data)
assert.Equal(t, nil, err)
assert.NotEqual(t, encrypted, data)
decrypted, err := c.Decrypt(encrypted)
assert.Equal(t, nil, err)
assert.Equal(t, data, decrypted)
// GCM is authenticated - this should lead to message authentication failed
_, err = gcm.Decrypt(encrypted)
assert.Error(t, err)
}
}
}
func TestDecryptCFBWrongSecret(t *testing.T) {
var err error
secret1 := []byte("0123456789abcdefghijklmnopqrstuv")
secret2 := []byte("9876543210abcdefghijklmnopqrstuv")
c1, err := NewCFBCipher(secret1)
assert.Equal(t, nil, err)
c2, err := NewCFBCipher(secret2)
assert.Equal(t, nil, err)
data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df")
ciphertext, err := c1.Encrypt(data)
assert.Equal(t, nil, err)
wrongData, err := c2.Decrypt(ciphertext)
assert.Equal(t, nil, err)
assert.NotEqual(t, data, wrongData)
}
func TestEncodeIntoAndDecodeIntoAccessToken(t *testing.T) {
const secret = "0123456789abcdefghijklmnopqrstuv"
c, err := NewCipher([]byte(secret))