diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index e2dbfa1a..8eddc7d0 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -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 == "" { diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index 8abfcb2e..85ec5ce2 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -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))