Skip to content

Commit

Permalink
Added AES/GCM encryption. Using base64 encoding for encryption keys. …
Browse files Browse the repository at this point in the history
…Releasing v0.7.2
  • Loading branch information
pdt256 committed Jan 26, 2021
1 parent c33bd16 commit e4e0265
Show file tree
Hide file tree
Showing 13 changed files with 512 additions and 285 deletions.
139 changes: 139 additions & 0 deletions pkg/crypto/aes/aestest/verify_aes_encryption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package aestest

import (
"log"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/inklabs/rangedb/pkg/crypto"
"github.com/inklabs/rangedb/pkg/crypto/aes"
)

const (
PlainText = "lorem ipsum"
ValidAES256Base64Key = "QTwR7N42uXvGBqtdbeKfjWUjXWngnhq5xUBcE6pI4Zs="
ValidAESGCMBase64CipherText = "Glq32jvn9nPO/pqxN9p3YQT4pvoZuV4aQCOy/TIdEtqW8vtGMnsG"
InvalidAESLengthBase64Key = "aW52LWtleQ=="
InvalidBase64Key = "invalid-"
InvalidBase64CipherText = "."
EmptyBase64CipherText = ""
)

func VerifyAESEncryption(t *testing.T, encryptor crypto.Encryptor) {
encrypt, _ := aes.NewCBCPKCS5Padding().Encrypt(ValidAES256Base64Key, PlainText)
log.Print(encrypt)
t.Run("encrypt/decrypt string", func(t *testing.T) {
tests := []struct {
keyLength string
key string
}{
{keyLength: "AES-128", key: "awXrrjBxcHjJGtQGbP6eaQ=="},
{keyLength: "AES-192", key: "ahHVth1qUbB5VwjRg2V73Fuki46t7Jor"},
{keyLength: "AES-256", key: "QTwR7N42uXvGBqtdbeKfjWUjXWngnhq5xUBcE6pI4Zs="},
}

for _, tc := range tests {
t.Run(tc.keyLength, func(t *testing.T) {
// Given

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

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

t.Run("errors", func(t *testing.T) {
t.Run("encrypt", func(t *testing.T) {
t.Run("from invalid base64 key", func(t *testing.T) {
// Given
// When
decryptedValue, err := encryptor.Encrypt(InvalidBase64Key, PlainText)

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

t.Run("from invalid key size", func(t *testing.T) {
// Given
// When
decryptedValue, err := encryptor.Encrypt(InvalidAESLengthBase64Key, PlainText)

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

t.Run("decrypt", func(t *testing.T) {
t.Run("from invalid base64 key", func(t *testing.T) {
// Given
// When
decryptedValue, err := encryptor.Decrypt(InvalidBase64Key, ValidAESGCMBase64CipherText)

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

t.Run("from invalid base64 cipher text", func(t *testing.T) {
// Given
// When
decryptedValue, err := encryptor.Decrypt(ValidAES256Base64Key, InvalidBase64CipherText)

// 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
// When
decryptedValue, err := encryptor.Decrypt(ValidAES256Base64Key, EmptyBase64CipherText)

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

t.Run("from invalid key size", func(t *testing.T) {
// Given
// When
decryptedValue, err := encryptor.Decrypt(InvalidAESLengthBase64Key, ValidAESGCMBase64CipherText)

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

func AESEncryptorBenchmark(b *testing.B, encryptor crypto.Encryptor, cipherText string) {
b.Run("encrypt", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := encryptor.Encrypt(ValidAES256Base64Key, PlainText)
if err != nil {
require.NoError(b, err)
}
}
})

b.Run("decrypt", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := encryptor.Decrypt(ValidAES256Base64Key, cipherText)
if err != nil {
require.NoError(b, err)
}
}
})
}
105 changes: 105 additions & 0 deletions pkg/crypto/aes/cbc_pkcs5_padding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package aes

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
)

type cbcPKCS5Padding struct{}

// NewCBCPKCS5Padding constructs an AES/CBC/PKCS5Padding encryption engine.
func NewCBCPKCS5Padding() *cbcPKCS5Padding {
return &cbcPKCS5Padding{}
}

// Encrypt returns AES/CBC/PKCS5Padding base64 cipher text.
// The key argument should be the base64 encoded AES key,
// either 16, 24, or 32 bytes to select
// AES-128, AES-192, or AES-256.
func (e *cbcPKCS5Padding) Encrypt(base64Key, plainText string) (string, error) {
key, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
return "", err
}

cipherText, err := e.encrypt([]byte(plainText), key)
base64CipherText := base64.StdEncoding.EncodeToString(cipherText)
return base64CipherText, err
}

// Decrypt returns a decrypted string from AES/CBC/PKCS5Padding base64 cipher text.
func (e *cbcPKCS5Padding) Decrypt(base64Key, base64CipherText string) (string, error) {
key, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
return "", err
}

cipherText, err := base64.StdEncoding.DecodeString(base64CipherText)
if err != nil {
return "", err
}

decryptedData, err := e.decrypt(key, cipherText)
return string(decryptedData), err
}

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

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

initializationVector := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, initializationVector); err != nil {
return nil, err
}

cbcEncrypter := cipher.NewCBCEncrypter(cipherBlock, initializationVector)
cbcEncrypter.CryptBlocks(cipherText, plainTextWithPadding)

ivAndCipherText := append(initializationVector, cipherText...)

return ivAndCipherText, nil
}

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

cipherBlock, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

buf := bytes.NewReader(cipherText)
initializationVector := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(buf, initializationVector); 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 pkcs5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padText...)
}

func pkcs5Trimming(value []byte) []byte {
padding := value[len(value)-1]
return value[:len(value)-int(padding)]
}
17 changes: 17 additions & 0 deletions pkg/crypto/aes/cbc_pkcs5_padding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package aes_test

import (
"testing"

"github.com/inklabs/rangedb/pkg/crypto/aes"
"github.com/inklabs/rangedb/pkg/crypto/aes/aestest"
)

func TestCBCPKCS5Padding(t *testing.T) {
aestest.VerifyAESEncryption(t, aes.NewCBCPKCS5Padding())
}

func BenchmarkCBCPKCS5Padding(b *testing.B) {
const AESCBCPKCS5PaddingBase64CipherText = "hZUAwIGBqAzjDlMDIGvztI0du4Vedspv1IHD48iKfg4="
aestest.AESEncryptorBenchmark(b, aes.NewCBCPKCS5Padding(), AESCBCPKCS5PaddingBase64CipherText)
}
114 changes: 114 additions & 0 deletions pkg/crypto/aes/gcm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package aes

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
)

// GCMOption defines functional option parameters for GCM.
type GCMOption func(*GCM)

// WithRandReader is a functional option to inject a random reader.
func WithRandReader(randReader io.Reader) GCMOption {
return func(e *GCM) {
e.randReader = randReader
}
}

type GCM struct {
randReader io.Reader
}

// NewGCM constructs an AES/GCM encryption engine.
func NewGCM(options ...GCMOption) *GCM {
e := &GCM{
randReader: rand.Reader,
}

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

return e
}

// Encrypt returns AES/GCM base64 cipher text.
// The key argument should be the base64 encoded AES key,
// either 16, 24, or 32 bytes to select
// AES-128, AES-192, or AES-256.
func (e *GCM) Encrypt(base64Key, plainText string) (string, error) {
key, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
return "", err
}

cipherText, err := e.encrypt([]byte(plainText), key)
base64CipherText := base64.StdEncoding.EncodeToString(cipherText)
return base64CipherText, err
}

// Decrypt returns a decrypted string from AES/GCM base64 cipher text.
func (e *GCM) Decrypt(base64Key, base64CipherText string) (string, error) {
key, err := base64.StdEncoding.DecodeString(base64Key)
if err != nil {
return "", err
}

cipherText, err := base64.StdEncoding.DecodeString(base64CipherText)
if err != nil {
return "", err
}

decryptedData, err := e.decrypt(key, cipherText)
return string(decryptedData), err
}

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

gcm, err := cipher.NewGCM(cipherBlock)
if err != nil {
return nil, err
}

nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(e.randReader, nonce); err != nil {
return nil, err
}

sealedCipherText := gcm.Seal(nonce, nonce, plainText, nil)
return sealedCipherText, nil
}

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

cipherBlock, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

gcm, err := cipher.NewGCM(cipherBlock)
if err != nil {
return nil, err
}

nonceSize := gcm.NonceSize()
nonce, cipherText := sealedCipherText[:nonceSize], sealedCipherText[nonceSize:]

plainText, err := gcm.Open(nil, nonce, cipherText, nil)
if err != nil {
return nil, err
}

return plainText, nil
}

0 comments on commit e4e0265

Please sign in to comment.