diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e2b42c533f8b692041e4eb36f97c1640707fd6cc..0d61ea34de8cddb052bf655bdf9b73b9053da4f8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -203,4 +203,4 @@ integration-test-kms: before_script: - bash -c "$(curl -sL https://get-gnmic.openconfig.net)" script: - - go test -p 1 ./integration-tests/code/* + - go test -p 1 ./integration-tests/code/getKSAKeyTest diff --git a/goKMS/kms/crypto/crypto.go b/goKMS/kms/crypto/crypto.go index 1fe6c939f1a3e1931cbd8261fd5fafc418cadf49..960c8c921c2750029e30bb856fb7df99024e42a8 100644 --- a/goKMS/kms/crypto/crypto.go +++ b/goKMS/kms/crypto/crypto.go @@ -7,50 +7,78 @@ import ( "io" ) +// CryptoAlgorithm is an interface that provides the methods required for +// encryption and decryption of data. +// Currently only AES is supported, but this could be extended to support other +// algorithms in the future. type CryptoAlgorithm interface { Encrypt(plaintext []byte, key []byte) ([]byte, []byte, error) Decrypt(nonce, ciphertext []byte, key []byte) ([]byte, error) } -// AES. +// AES is an implementation of the CryptoAlgorithm interface. +// AES provides the methods required for performing symmetric key encryption +// and decryption using the AES algorithm. +// +// For this the aes package from the Go standard library is used. type AES struct{} +// NewAES creates a new instance of a AES struct. func NewAES() *AES { return &AES{} } +// Encrypt encrypts the plaintext using a provided key. +// The key should have a length of 16, 24 or 32 bytes to select AES-128, +// AES-192 or AES-256. +// The method returns the nonce, the encrypted output and an error if something +// went wrong. func (a *AES) Encrypt(plaintext []byte, key []byte) ([]byte, []byte, error) { + // create a new cipher block from the key c, err := aes.NewCipher(key) if err != nil { return nil, nil, err } + // create a new block cipher wrapped in GCM with default nonce (12 + // bytes) and tag size (16 bytes). gcm, err := cipher.NewGCM(c) if err != nil { return nil, nil, err } + // generate a random nonce of nonce size (12 bytes) nonce := make([]byte, gcm.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return nil, nil, err } + // Encrypt the plaintext using AES-GCM + // Destination is set to nil, therefore seal only contains the + // ciphertext with the tag appended. seal := gcm.Seal(nil, nonce, plaintext, nil) return nonce, seal, nil } +// Decrypt decrypts the ciphertext using the provided key and nonce. +// The key should have a length of 16, 24 or 32 bytes to select AES-128, +// AES-192 or AES-256. +// The method returns the decrypted input. func (a *AES) Decrypt(nonce, ciphertext []byte, key []byte) ([]byte, error) { + // create a new cipher block from the key c, err := aes.NewCipher(key) if err != nil { return nil, err } - // Note: This works under the assumption of every other implementation using the commonly used nonce size of 12 bytes. + // create a new block cipher wrapped in GCM with default nonce (12 + // bytes) and tag size (16 bytes). gcm, err := cipher.NewGCM(c) if err != nil { return nil, err } + // Decrypt the ciphertext using AES-GCM return gcm.Open(nil, nonce, ciphertext, nil) } diff --git a/goKMS/kms/crypto/crypto_test.go b/goKMS/kms/crypto/crypto_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6c7afe7fab79cc2265d33fe2071d6b5129e5ecbb --- /dev/null +++ b/goKMS/kms/crypto/crypto_test.go @@ -0,0 +1,206 @@ +package crypto + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCrypto_AES_Encrypt(t *testing.T) { + tests := map[string]struct { + plaintext []byte + key []byte + error bool + expectedCipherTextLength int + }{ + "AES-128": { + plaintext: []byte("testMessage"), + key: []byte{0xaa, 0xdf, 0x4f, 0x20, 0x9e, 0x35, 0xe0, 0x9c, 0xde, 0x6f, 0xf8, 0x51, 0x29, 0x98, 0x49, 0xae}, + error: false, + expectedCipherTextLength: 27, + }, + "AES-192": { + plaintext: []byte("testMessage"), + key: []byte{0x67, 0xc8, 0x12, 0x60, 0x8, 0x1e, 0x1f, 0x2e, 0x1d, 0x58, 0x60, 0xb1, 0x9c, 0xf, 0x14, 0x4d, 0xe2, 0x9e, 0xd3, 0xc1, 0x9f, 0xa8, 0x9f, 0x59}, + error: false, + expectedCipherTextLength: 27, + }, + "AES-256": { + plaintext: []byte("testMessage"), + key: []byte{0xf6, 0x4e, 0x81, 0x5f, 0x90, 0x87, 0x78, 0x66, 0x33, 0x7b, 0xc, 0xe2, 0x8, 0xcd, 0xe, 0x49, 0xd1, 0x26, 0x4d, 0x35, 0xa6, 0x36, 0xde, 0x5c, 0x58, 0xfa, 0xa3, 0x83, 0xc0, 0xc9, 0x8c, 0xf}, + error: false, + expectedCipherTextLength: 27, + }, + "AES-256 longer plaintext": { + plaintext: []byte("testMessageThatIsLonger"), + key: []byte{0x54, 0x39, 0xc8, 0x71, 0x4e, 0x79, 0x27, 0x92, 0xa6, 0x1, 0xf0, 0xfc, 0xff, 0xa0, 0x3c, 0x76, 0x5f, 0x33, 0xc8, 0xa6, 0x42, 0x3c, 0x14, 0x67, 0x64, 0xbf, 0x22, 0xac, 0x84, 0x55, 0x9, 0x13}, + error: false, + expectedCipherTextLength: 39, + }, + "wrong key size": { + plaintext: []byte("testMessage"), + key: []byte{0xff, 0x99, 0x54, 0xec, 0xde, 0x7b, 0xf7, 0x66, 0x2f, 0xff, 0xbc, 0xe0, 0x1e, 0x75, 0x4, 0xb2, 0x1d, 0x4e, 0x46, 0x9b, 0x71, 0x4e, 0xf9, 0xe7, 0xc1, 0x20, 0xa1, 0x58, 0xbe, 0x52}, + error: true, + expectedCipherTextLength: 0, + }, + } + + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + t.Parallel() + + aes := NewAES() + nonce, cipherText, err := aes.Encrypt(test.plaintext, test.key) + if test.error { + assert.Error(t, err) + assert.Len(t, nonce, 0) + } else { + assert.NoError(t, err) + assert.Len(t, nonce, 12) + } + assert.Len(t, cipherText, test.expectedCipherTextLength) + }) + } +} + +func TestCrypto_AES_Decrypt(t *testing.T) { + tests := map[string]struct { + cipherText []byte + key []byte + nonce []byte + error bool + expectedPlainText string + }{ + "AES-128": { + cipherText: []byte{0x7b, 0x45, 0x4b, 0x44, 0xcf, 0xc6, 0x5b, 0xe8, 0x7b, 0xc0, 0x10, 0x36, 0xea, 0x41, 0xc4, 0x25, 0x32, 0xe7, 0xe7, 0x9, 0x38, 0xca, 0xf9, 0x47, 0x8d, 0xdf, 0xac}, + key: []byte{0xaa, 0xdf, 0x4f, 0x20, 0x9e, 0x35, 0xe0, 0x9c, 0xde, 0x6f, 0xf8, 0x51, 0x29, 0x98, 0x49, 0xae}, + nonce: []byte{0x1a, 0x1e, 0xeb, 0x20, 0x8e, 0xd4, 0xbb, 0x77, 0x58, 0x6a, 0xd, 0x82}, + error: false, + expectedPlainText: "testMessage", + }, + "AES-128 faulty nonce": { + cipherText: []byte{0x7b, 0x45, 0x4b, 0x44, 0xcf, 0xc6, 0x5b, 0xe8, 0x7b, 0xc0, 0x10, 0x36, 0xea, 0x41, 0xc4, 0x25, 0x32, 0xe7, 0xe7, 0x9, 0x38, 0xca, 0xf9, 0x47, 0x8d, 0xdf, 0xac}, + key: []byte{0xaa, 0xdf, 0x4f, 0x20, 0x9e, 0x35, 0xe0, 0x9c, 0xde, 0x6f, 0xf8, 0x51, 0x29, 0x98, 0x49, 0xae}, + nonce: []byte{0x91, 0x66, 0x68, 0x5b, 0x64, 0x84, 0x5a, 0x81, 0xfd, 0xce, 0x89, 0x93}, + error: true, + expectedPlainText: "", + }, + "AES-128 faulty key": { + cipherText: []byte{0x7b, 0x45, 0x4b, 0x44, 0xcf, 0xc6, 0x5b, 0xe8, 0x7b, 0xc0, 0x10, 0x36, 0xea, 0x41, 0xc4, 0x25, 0x32, 0xe7, 0xe7, 0x9, 0x38, 0xca, 0xf9, 0x47, 0x8d, 0xdf, 0xac}, + key: []byte{0x2b, 0x1c, 0xc9, 0x6d, 0xa2, 0x17, 0x25, 0x21, 0xa9, 0x9a, 0x8e, 0x17, 0x49, 0xc7, 0x3d, 0x32}, + nonce: []byte{0x1a, 0x1e, 0xeb, 0x20, 0x8e, 0xd4, 0xbb, 0x77, 0x58, 0x6a, 0xd, 0x82}, + error: true, + expectedPlainText: "", + }, + "AES-128 faulty cipherText": { + cipherText: []byte{0x94, 0x7f, 0xd2, 0xd1, 0x71, 0xf8, 0xe7, 0x31, 0x23, 0x37, 0xad, 0x88, 0xfa, 0x5c, 0xcc, 0xdd, 0xd, 0xc2, 0x78, 0xee, 0x4d, 0xbe, 0xb, 0x2e, 0xf4, 0x77, 0xda}, + key: []byte{0xaa, 0xdf, 0x4f, 0x20, 0x9e, 0x35, 0xe0, 0x9c, 0xde, 0x6f, 0xf8, 0x51, 0x29, 0x98, 0x49, 0xae}, + nonce: []byte{0x1a, 0x1e, 0xeb, 0x20, 0x8e, 0xd4, 0xbb, 0x77, 0x58, 0x6a, 0xd, 0x82}, + error: true, + expectedPlainText: "", + }, + "AES-192": { + cipherText: []byte{0x80, 0x8, 0xa9, 0x68, 0x51, 0x6a, 0x93, 0xf8, 0xc7, 0x96, 0xb1, 0xc4, 0x9d, 0xf8, 0x8c, 0xde, 0x43, 0x20, 0xe9, 0x11, 0x7a, 0x6e, 0x4c, 0x74, 0xb1, 0xf8, 0xa4}, + key: []byte{0x67, 0xc8, 0x12, 0x60, 0x8, 0x1e, 0x1f, 0x2e, 0x1d, 0x58, 0x60, 0xb1, 0x9c, 0xf, 0x14, 0x4d, 0xe2, 0x9e, 0xd3, 0xc1, 0x9f, 0xa8, 0x9f, 0x59}, + nonce: []byte{0x33, 0x55, 0xb8, 0x34, 0x3b, 0x4, 0xc5, 0xd7, 0xef, 0x8b, 0x49, 0x9e}, + error: false, + expectedPlainText: "testMessage", + }, + "AES-192 faulty nonce": { + cipherText: []byte{0x80, 0x8, 0xa9, 0x68, 0x51, 0x6a, 0x93, 0xf8, 0xc7, 0x96, 0xb1, 0xc4, 0x9d, 0xf8, 0x8c, 0xde, 0x43, 0x20, 0xe9, 0x11, 0x7a, 0x6e, 0x4c, 0x74, 0xb1, 0xf8, 0xa4}, + key: []byte{0x67, 0xc8, 0x12, 0x60, 0x8, 0x1e, 0x1f, 0x2e, 0x1d, 0x58, 0x60, 0xb1, 0x9c, 0xf, 0x14, 0x4d, 0xe2, 0x9e, 0xd3, 0xc1, 0x9f, 0xa8, 0x9f, 0x59}, + nonce: []byte{0x91, 0x66, 0x68, 0x5b, 0x64, 0x84, 0x5a, 0x81, 0xfd, 0xce, 0x89, 0x93}, + error: true, + expectedPlainText: "", + }, + "AES-192 faulty key": { + cipherText: []byte{0x80, 0x8, 0xa9, 0x68, 0x51, 0x6a, 0x93, 0xf8, 0xc7, 0x96, 0xb1, 0xc4, 0x9d, 0xf8, 0x8c, 0xde, 0x43, 0x20, 0xe9, 0x11, 0x7a, 0x6e, 0x4c, 0x74, 0xb1, 0xf8, 0xa4}, + key: []byte{0x2b, 0x1c, 0xc9, 0x6d, 0xa2, 0x17, 0x25, 0x21, 0xa9, 0x9a, 0x8e, 0x17, 0x49, 0xc7, 0x3d, 0x32, 0x24, 0x78, 0xb2, 0xc1, 0x15, 0x9f, 0x8b, 0xf3}, + nonce: []byte{0x33, 0x55, 0xb8, 0x34, 0x3b, 0x4, 0xc5, 0xd7, 0xef, 0x8b, 0x49, 0x9e}, + error: true, + expectedPlainText: "", + }, + "AES-192 faulty cipherText": { + cipherText: []byte{0x94, 0x7f, 0xd2, 0xd1, 0x71, 0xf8, 0xe7, 0x31, 0x23, 0x37, 0xad, 0x88, 0xfa, 0x5c, 0xcc, 0xdd, 0xd, 0xc2, 0x78, 0xee, 0x4d, 0xbe, 0xb, 0x2e, 0xf4, 0x77, 0xda}, + key: []byte{0x67, 0xc8, 0x12, 0x60, 0x8, 0x1e, 0x1f, 0x2e, 0x1d, 0x58, 0x60, 0xb1, 0x9c, 0xf, 0x14, 0x4d, 0xe2, 0x9e, 0xd3, 0xc1, 0x9f, 0xa8, 0x9f, 0x59}, + nonce: []byte{0x33, 0x55, 0xb8, 0x34, 0x3b, 0x4, 0xc5, 0xd7, 0xef, 0x8b, 0x49, 0x9e}, + error: true, + expectedPlainText: "", + }, + "AES-256": { + cipherText: []byte{0xea, 0x80, 0x9c, 0xd8, 0x21, 0x2b, 0x50, 0x42, 0x8, 0x4d, 0xd0, 0xb3, 0x6b, 0x48, 0x1e, 0x90, 0xd0, 0xa, 0x76, 0x85, 0x58, 0xc2, 0x39, 0xfb, 0x66, 0xe7, 0x5}, + key: []byte{0xf6, 0x4e, 0x81, 0x5f, 0x90, 0x87, 0x78, 0x66, 0x33, 0x7b, 0xc, 0xe2, 0x8, 0xcd, 0xe, 0x49, 0xd1, 0x26, 0x4d, 0x35, 0xa6, 0x36, 0xde, 0x5c, 0x58, 0xfa, 0xa3, 0x83, 0xc0, 0xc9, 0x8c, 0xf}, + nonce: []byte{0x59, 0xf5, 0x6, 0xa8, 0x82, 0x2, 0xa2, 0x3d, 0x28, 0xac, 0x85, 0x45}, + error: false, + expectedPlainText: "testMessage", + }, + "AES-256 faulty nonce": { + cipherText: []byte{0xea, 0x80, 0x9c, 0xd8, 0x21, 0x2b, 0x50, 0x42, 0x8, 0x4d, 0xd0, 0xb3, 0x6b, 0x48, 0x1e, 0x90, 0xd0, 0xa, 0x76, 0x85, 0x58, 0xc2, 0x39, 0xfb, 0x66, 0xe7, 0x5}, + key: []byte{0xf6, 0x4e, 0x81, 0x5f, 0x90, 0x87, 0x78, 0x66, 0x33, 0x7b, 0xc, 0xe2, 0x8, 0xcd, 0xe, 0x49, 0xd1, 0x26, 0x4d, 0x35, 0xa6, 0x36, 0xde, 0x5c, 0x58, 0xfa, 0xa3, 0x83, 0xc0, 0xc9, 0x8c, 0xf}, + nonce: []byte{0x91, 0x66, 0x68, 0x5b, 0x64, 0x84, 0x5a, 0x81, 0xfd, 0xce, 0x89, 0x93}, + error: true, + expectedPlainText: "", + }, + "AES-256 faulty key": { + cipherText: []byte{0xea, 0x80, 0x9c, 0xd8, 0x21, 0x2b, 0x50, 0x42, 0x8, 0x4d, 0xd0, 0xb3, 0x6b, 0x48, 0x1e, 0x90, 0xd0, 0xa, 0x76, 0x85, 0x58, 0xc2, 0x39, 0xfb, 0x66, 0xe7, 0x5}, + key: []byte{0x2b, 0x1c, 0xc9, 0x6d, 0xa2, 0x17, 0x25, 0x21, 0xa9, 0x9a, 0x8e, 0x17, 0x49, 0xc7, 0x3d, 0x32, 0x24, 0x78, 0xb2, 0xc1, 0x15, 0x9f, 0x8b, 0xf3, 0xa9, 0x54, 0xc4, 0x90, 0x26, 0x33, 0x9, 0x60}, + nonce: []byte{0x59, 0xf5, 0x6, 0xa8, 0x82, 0x2, 0xa2, 0x3d, 0x28, 0xac, 0x85, 0x45}, + error: true, + expectedPlainText: "", + }, + "AES-256 faulty cipherText": { + cipherText: []byte{0x94, 0x7f, 0xd2, 0xd1, 0x71, 0xf8, 0xe7, 0x31, 0x23, 0x37, 0xad, 0x88, 0xfa, 0x5c, 0xcc, 0xdd, 0xd, 0xc2, 0x78, 0xee, 0x4d, 0xbe, 0xb, 0x2e, 0xf4, 0x77, 0xda}, + key: []byte{0xf6, 0x4e, 0x81, 0x5f, 0x90, 0x87, 0x78, 0x66, 0x33, 0x7b, 0xc, 0xe2, 0x8, 0xcd, 0xe, 0x49, 0xd1, 0x26, 0x4d, 0x35, 0xa6, 0x36, 0xde, 0x5c, 0x58, 0xfa, 0xa3, 0x83, 0xc0, 0xc9, 0x8c, 0xf}, + nonce: []byte{0x59, 0xf5, 0x6, 0xa8, 0x82, 0x2, 0xa2, 0x3d, 0x28, 0xac, 0x85, 0x45}, + error: true, + expectedPlainText: "", + }, + "AES-256 longer cipherText": { + cipherText: []byte{0x44, 0x35, 0x7a, 0x70, 0x19, 0x31, 0x11, 0xbf, 0xab, 0xf1, 0x32, 0x9d, 0x7b, 0x73, 0xcc, 0x78, 0x7b, 0x5, 0xe7, 0x87, 0xcf, 0xd9, 0xe6, 0x28, 0xa8, 0x53, 0xbf, 0x70, 0x37, 0x64, 0x2f, 0x14, 0x2c, 0xc, 0xeb, 0x53, 0x1, 0x22, 0xd0}, + key: []byte{0x54, 0x39, 0xc8, 0x71, 0x4e, 0x79, 0x27, 0x92, 0xa6, 0x1, 0xf0, 0xfc, 0xff, 0xa0, 0x3c, 0x76, 0x5f, 0x33, 0xc8, 0xa6, 0x42, 0x3c, 0x14, 0x67, 0x64, 0xbf, 0x22, 0xac, 0x84, 0x55, 0x9, 0x13}, + nonce: []byte{0x59, 0xf6, 0x94, 0xeb, 0x6a, 0x5a, 0xdc, 0x3a, 0x89, 0xa9, 0xbb, 0x53}, + error: false, + expectedPlainText: "testMessageThatIsLonger", + }, + "wrong key size": { + cipherText: []byte{0xea, 0x80, 0x9c, 0xd8, 0x21, 0x2b, 0x50, 0x42, 0x8, 0x4d, 0xd0, 0xb3, 0x6b, 0x48, 0x1e, 0x90, 0xd0, 0xa, 0x76, 0x85, 0x58, 0xc2, 0x39, 0xfb, 0x66, 0xe7, 0x5}, + key: []byte{0xf6, 0x4e, 0x81, 0x5f, 0x90, 0x87, 0x78, 0x66, 0x33, 0x7b, 0xc, 0xe2, 0x8, 0xcd, 0xe, 0x49, 0xd1, 0x26, 0x4d, 0x35, 0xa6, 0x36, 0xde, 0x5c, 0x58, 0xfa, 0xa3, 0x83, 0xc0, 0xc9}, + nonce: []byte{0x59, 0xf5, 0x6, 0xa8, 0x82, 0x2, 0xa2, 0x3d, 0x28, 0xac, 0x85, 0x45}, + error: true, + expectedPlainText: "", + }, + } + + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + t.Parallel() + + aes := NewAES() + plainText, err := aes.Decrypt(test.nonce, test.cipherText, test.key) + if test.error { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, test.expectedPlainText, string(plainText)) + }) + } +} + +func TestCrypto_AES_EncryptAndDecryptPlaintext(t *testing.T) { + secret := []byte("this is a secret") + key := []byte{0xfe, 0x34, 0x64, 0x9e, 0xdf, 0x1a, 0xf1, 0xc, 0xb7, 0x28, 0xee, 0x98, 0xe7, 0x7, 0x40, 0x8f, 0x3b, 0x8, 0x9a, 0xad, 0x45, 0x7a, 0x21, 0xe8, 0x84, 0x79, 0xc5, 0x1b, 0x25, 0x13, 0xa2, 0x3c} + + aes := NewAES() + + // encrypt the secret with encrypt method + nonce, encryptedSecret, err := aes.Encrypt(secret, key) + assert.NoError(t, err) + + // decrypt the encryptedSecret with decrypt method + decryptedSecret, err := aes.Decrypt(nonce, encryptedSecret, key) + assert.NoError(t, err) + assert.Equal(t, secret, decryptedSecret) +} diff --git a/goKMS/kms/crypto/utils.go b/goKMS/kms/crypto/utils.go index 349b73011354d95d605ae62c264561f06bc0d950..5487f423f639e9415c3a2e7fd8ed66cc8b4a46c9 100644 --- a/goKMS/kms/crypto/utils.go +++ b/goKMS/kms/crypto/utils.go @@ -7,6 +7,8 @@ import ( "github.com/google/uuid" ) +// Key is a struct that holds a key as a byte array and as a base64 encoded +// string and the id of the key. type Key struct { // ID is the id of the key ID uuid.UUID @@ -16,13 +18,18 @@ type Key struct { KeyAsBase64 string } +// Random256BitKey generates a random 256 bit key and returns it as a Key +// struct. func Random256BitKey() (*Key, error) { + // Create a new byte array with a length of 32 bytes b := make([]byte, 32) + // fill the byte array with random bytes _, err := rand.Read(b) if err != nil { return nil, err } + // Encode the byte array to a base64 encoded string keyAsBase64String := base64.StdEncoding.EncodeToString(b) return &Key{ diff --git a/goKMS/kms/crypto/utils_test.go b/goKMS/kms/crypto/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9c6122090e9f08302f340cb7f53b29c858c343f4 --- /dev/null +++ b/goKMS/kms/crypto/utils_test.go @@ -0,0 +1,30 @@ +package crypto + +import ( + "encoding/base64" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestCrypto_Random256BitKey(t *testing.T) { + key, err := Random256BitKey() + assert.NoError(t, err) + + // check key id is not nil + assert.NotEqual(t, uuid.Nil, key.ID) + + // check for correct key length + assert.Len(t, key.Key, 32) + assert.NotEqual(t, make([]byte, 32), key.Key) + + // decode base64 encoded string + keyFromBase64, err := base64.StdEncoding.DecodeString(key.KeyAsBase64) + assert.NoError(t, err) + + // check for correct key length + assert.Len(t, keyFromBase64, 32) + // check if both keys represent the same value + assert.Equal(t, key.Key, keyFromBase64) +}