-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added AES/GCM encryption. Using base64 encoding for encryption keys. …
…Releasing v0.7.2
- Loading branch information
Showing
13 changed files
with
512 additions
and
285 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.