Skip to content

Commit

Permalink
Changed AES initialization vector to be generated via crypto/rand and…
Browse files Browse the repository at this point in the history
… prepended to the cipher text.

Releasing v0.7.1
  • Loading branch information
pdt256 committed Jan 26, 2021
1 parent 836e24b commit c33bd16
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 159 deletions.
100 changes: 77 additions & 23 deletions pkg/crypto/aes_encryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,56 +4,84 @@ import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
)

type RandReader func(b []byte) (n int, err error)

// Option defines functional option parameters for aesEncryption.
type Option func(*aesEncryption)

// WithRandReader is a functional option to inject a random reader.
func WithRandReader(randReader RandReader) Option {
return func(e *aesEncryption) {
e.randReader = randReader
}
}

type aesEncryption struct {
initializationVector []byte
randReader RandReader
}

// NewAESEncryption constructs an AES/CBC/PKCS5Padding encryption engine.
func NewAESEncryption(initializationVector []byte) *aesEncryption {
return &aesEncryption{
initializationVector: initializationVector,
func NewAESEncryption(options ...Option) *aesEncryption {
e := &aesEncryption{
randReader: rand.Read,
}

for _, option := range options {
option(e)
}

return e
}

// Encrypt returns AES/CBC/PKCS5Padding/base64 encoded string.
func (i *aesEncryption) Encrypt(key, data string) (string, error) {
encryptedData, err := i.encrypt([]byte(data), []byte(key))
base64EncodedData := base64.StdEncoding.EncodeToString(encryptedData)
return base64EncodedData, err
// Encrypt returns AES/CBC/PKCS5Padding base64 cipher text.
// The key argument should be the AES key,
// either 16, 24, or 32 bytes to select
// AES-128, AES-192, or AES-256.
func (e *aesEncryption) Encrypt(key, data string) (string, error) {
cipherText, err := e.encrypt([]byte(data), []byte(key))
base64CipherText := base64.StdEncoding.EncodeToString(cipherText)
return base64CipherText, err
}

// Decrypt returns a decrypted string from AES/CBC/PKCS5Padding/base64 encoded value.
func (i *aesEncryption) Decrypt(key, base64EncryptedData string) (string, error) {
encryptedData, err := base64.StdEncoding.DecodeString(base64EncryptedData)
// Decrypt returns a decrypted string from AES/CBC/PKCS5Padding base64 cipher text.
func (e *aesEncryption) Decrypt(key, base64CipherText string) (string, error) {
cipherText, err := base64.StdEncoding.DecodeString(base64CipherText)
if err != nil {
return "", err
}

decryptedData, err := i.decrypt(encryptedData, []byte(key))
decryptedData, err := e.decrypt(cipherText, []byte(key))
return string(decryptedData), err
}

func (i *aesEncryption) encrypt(src, key []byte) ([]byte, error) {
func (e *aesEncryption) encrypt(src, key []byte) ([]byte, error) {
cipherBlock, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

plainText := pkcs5Padding(src, cipherBlock.BlockSize())
encryptedData := make([]byte, len(plainText))
cipherText := make([]byte, len(plainText))

initializationVector, err := e.randomIV()
if err != nil {
return nil, err
}
cbcEncrypter := cipher.NewCBCEncrypter(cipherBlock, initializationVector)
cbcEncrypter.CryptBlocks(cipherText, plainText)

cbcEncrypter := cipher.NewCBCEncrypter(cipherBlock, i.initializationVector)
cbcEncrypter.CryptBlocks(encryptedData, plainText)
ivAndCipherText := append(initializationVector, cipherText...)

return encryptedData, nil
return ivAndCipherText, nil
}

func (i *aesEncryption) decrypt(encryptedData []byte, key []byte) ([]byte, error) {
if len(encryptedData) == 0 {
func (e *aesEncryption) decrypt(cipherText []byte, key []byte) ([]byte, error) {
if len(cipherText) == 0 {
return nil, fmt.Errorf("encrypted data empty")
}

Expand All @@ -62,13 +90,27 @@ func (i *aesEncryption) decrypt(encryptedData []byte, key []byte) ([]byte, error
return nil, err
}

ecb := cipher.NewCBCDecrypter(cipherBlock, i.initializationVector)
decrypted := make([]byte, len(encryptedData))
ecb.CryptBlocks(decrypted, encryptedData)
initializationVector, err := readIV(cipherText)
if err != nil {
return nil, err
}

ecb := cipher.NewCBCDecrypter(cipherBlock, initializationVector)
decrypted := make([]byte, len(cipherText)-aes.BlockSize)
ecb.CryptBlocks(decrypted, cipherText[aes.BlockSize:])

return pkcs5Trimming(decrypted), nil
}

func (e *aesEncryption) randomIV() ([]byte, error) {
iv := make([]byte, aes.BlockSize)
if _, err := e.randReader(iv); err != nil {
return nil, err
}

return iv, nil
}

func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
Expand All @@ -79,3 +121,15 @@ func pkcs5Trimming(value []byte) []byte {
padding := value[len(value)-1]
return value[:len(value)-int(padding)]
}

// input contains the IV in the first 16 bytes,
// then the actual ciphertext in the rest of the buffer.
func readIV(input []byte) ([]byte, error) {
buf := bytes.NewReader(input)
iv := make([]byte, aes.BlockSize)
if _, err := buf.Read(iv); err != nil {
return nil, err
}

return iv, nil
}
213 changes: 103 additions & 110 deletions pkg/crypto/aes_encryption_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,136 +10,129 @@ import (
)

func TestAESEncryption(t *testing.T) {
t.Run("encrypt", func(t *testing.T) {
t.Run("encrypt string", func(t *testing.T) {
// Given
const (
iv = "encryptionIntVec"
key = "aesEncryptionKey"
text = "password"
)
aesEncryption := crypto.NewAESEncryption([]byte(iv))

// When
encryptedValue, err := aesEncryption.Encrypt(key, text)

// Then
require.NoError(t, err)
expected := "AIDTAIiCazaQavILI07rtA=="
assert.Equal(t, expected, encryptedValue)
})
t.Run("encrypt/decrypt string", func(t *testing.T) {
const text = "lorem ipsum"
tests := []struct {
keyLength string
key string
}{
{keyLength: "AES-128", key: "25b7ec6b03f14446"},
{keyLength: "AES-192", key: "4d1e9a28a2a3479a87c50bc6"},
{keyLength: "AES-256", key: "af51295ce958410ca61b123954b7ca71"},
}

t.Run("errors from invalid key", func(t *testing.T) {
// Given
const (
iv = "1234567890123456"
key = "inv-key"
text = "lorem ipsum"
)
aesEncryption := crypto.NewAESEncryption([]byte(iv))

// When
decryptedValue, err := aesEncryption.Encrypt(key, text)

// Then
require.EqualError(t, err, "crypto/aes: invalid key size 7")
assert.Equal(t, "", decryptedValue)
})
for _, tc := range tests {
t.Run(tc.keyLength, func(t *testing.T) {
// Given
aesEncryption := crypto.NewAESEncryption()

// When
encryptedValue, err := aesEncryption.Encrypt(tc.key, text)
require.NoError(t, err)
assert.NotEqual(t, text, encryptedValue)

// Then
decryptedValue, err := aesEncryption.Decrypt(tc.key, encryptedValue)
require.NoError(t, err)
assert.Equal(t, text, decryptedValue)
})
}
})

t.Run("decrypt", func(t *testing.T) {
t.Run("errors from invalid base64 data", func(t *testing.T) {
// Given
const (
iv = "1234567890123456"
key = "cf05dd80559342738d66977bc2aeb0e7"
invalidBase64Data = "."
)
aesEncryption := crypto.NewAESEncryption([]byte(iv))

// When
decryptedValue, err := aesEncryption.Decrypt(key, invalidBase64Data)

// Then
require.EqualError(t, err, "illegal base64 data at input byte 0")
assert.Equal(t, "", decryptedValue)
t.Run("errors", func(t *testing.T) {
t.Run("encrypt", func(t *testing.T) {
t.Run("from invalid key size", func(t *testing.T) {
// Given
const (
key = "inv-key"
text = "lorem ipsum"
)
aesEncryption := crypto.NewAESEncryption()

// When
decryptedValue, err := aesEncryption.Encrypt(key, text)

// Then
require.EqualError(t, err, "crypto/aes: invalid key size 7")
assert.Equal(t, "", decryptedValue)
})
})

t.Run("errors from empty base64 data", func(t *testing.T) {
// Given
const (
iv = "1234567890123456"
key = "cf05dd80559342738d66977bc2aeb0e7"
emptyBase64Data = ""
)
aesEncryption := crypto.NewAESEncryption([]byte(iv))

// When
decryptedValue, err := aesEncryption.Decrypt(key, emptyBase64Data)

// Then
require.EqualError(t, err, "encrypted data empty")
assert.Equal(t, "", decryptedValue)
t.Run("decrypt", func(t *testing.T) {
t.Run("from invalid base64 cipher text", func(t *testing.T) {
// Given
const (
key = "cf05dd80559342738d66977bc2aeb0e7"
invalidCipherText = "."
)
aesEncryption := crypto.NewAESEncryption()

// When
decryptedValue, err := aesEncryption.Decrypt(key, invalidCipherText)

// Then
require.EqualError(t, err, "illegal base64 data at input byte 0")
assert.Equal(t, "", decryptedValue)
})

t.Run("from empty base64 cipher text", func(t *testing.T) {
// Given
const (
key = "cf05dd80559342738d66977bc2aeb0e7"
emptyCipherText = ""
)
aesEncryption := crypto.NewAESEncryption()

// When
decryptedValue, err := aesEncryption.Decrypt(key, emptyCipherText)

// Then
require.EqualError(t, err, "encrypted data empty")
assert.Equal(t, "", decryptedValue)
})

t.Run("from invalid key size", func(t *testing.T) {
// Given
const (
key = "inv-key"
cipherText = "e0vf3bslT7TmgKCy1geZ+r70b7gcIvVk1MAfKF9iDk4="
)
aesEncryption := crypto.NewAESEncryption()

// When
decryptedValue, err := aesEncryption.Decrypt(key, cipherText)

// Then
require.EqualError(t, err, "crypto/aes: invalid key size 7")
assert.Equal(t, "", decryptedValue)
})
})

t.Run("errors from invalid key", func(t *testing.T) {
// Given
const (
iv = "1234567890123456"
key = "inv-key"
base64EncryptedData = "AIDTAIiCazaQavILI07rtA=="
)
aesEncryption := crypto.NewAESEncryption([]byte(iv))

// When
decryptedValue, err := aesEncryption.Decrypt(key, base64EncryptedData)

// Then
require.EqualError(t, err, "crypto/aes: invalid key size 7")
assert.Equal(t, "", decryptedValue)
})

})

t.Run("encrypt/decrypt string", func(t *testing.T) {
// Given
const (
iv = "1234567890123456"
key = "af51295ce958410ca61b123954b7ca71"
text = "lorem ipsum"
)
aesEncryption := crypto.NewAESEncryption([]byte(iv))

// When
encryptedValue, err := aesEncryption.Encrypt(key, text)
require.NoError(t, err)
assert.NotEqual(t, text, encryptedValue)

// Then
decryptedValue, err := aesEncryption.Decrypt(key, encryptedValue)
require.NoError(t, err)
assert.Equal(t, text, decryptedValue)
})
}

func BenchmarkAESEncryption(b *testing.B) {
const (
iv = "1234567890123456"
key = "24f8d5773ae944ce890ec4b09daf3054"
text = "lorem ipsum"
base64EncryptedData = "NO8RD1DFHuYHSyWbOiZlsw=="
key = "24f8d5773ae944ce890ec4b09daf3054"
text = "lorem ipsum"
cipherText = "e0vf3bslT7TmgKCy1geZ+r70b7gcIvVk1MAfKF9iDk4="
)
aesEncryption := crypto.NewAESEncryption([]byte(iv))
aesEncryption := crypto.NewAESEncryption()

b.Run("encrypt", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = aesEncryption.Encrypt(key, text)
_, err := aesEncryption.Encrypt(key, text)
if err != nil {
require.NoError(b, err)
}
}
})

b.Run("decrypt", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = aesEncryption.Decrypt(key, base64EncryptedData)
_, err := aesEncryption.Decrypt(key, cipherText)
if err != nil {
require.NoError(b, err)
}
}
})
}
Loading

0 comments on commit c33bd16

Please sign in to comment.