From 8fa13c6388ce76a6b878b54490eac61aa7d81165 Mon Sep 17 00:00:00 2001 From: Alex Ullrich Date: Mon, 1 Nov 2021 19:40:51 -0400 Subject: [PATCH] feat(transforms): new AES 256 Encryption Transform (#984) Add new transform using AES 256 Encryption with a SHA512 authentication mechanism. Fixes #968 Signed-off-by: Alex Ullrich --- internal/app/configurable.go | 30 ++-- internal/app/configurable_test.go | 3 + internal/etm/LICENSE | 21 +++ internal/etm/README.md | 3 + internal/etm/etm.go | 206 +++++++++++++++++++++++++++ internal/etm/etm_test.go | 162 +++++++++++++++++++++ pkg/transforms/aesprotection.go | 135 ++++++++++++++++++ pkg/transforms/aesprotection_test.go | 177 +++++++++++++++++++++++ pkg/transforms/encryption.go | 6 + 9 files changed, 734 insertions(+), 9 deletions(-) create mode 100644 internal/etm/LICENSE create mode 100644 internal/etm/README.md create mode 100644 internal/etm/etm.go create mode 100644 internal/etm/etm_test.go create mode 100644 pkg/transforms/aesprotection.go create mode 100644 pkg/transforms/aesprotection_test.go diff --git a/internal/app/configurable.go b/internal/app/configurable.go index 9159aa066..11b8e9e15 100644 --- a/internal/app/configurable.go +++ b/internal/app/configurable.go @@ -75,6 +75,7 @@ const ( CompressGZIP = "gzip" CompressZLIB = "zlib" EncryptAES = "aes" + EncryptAES256 = "aes256" Mode = "mode" BatchByCount = "bycount" BatchByTime = "bytime" @@ -317,21 +318,32 @@ func (app *Configurable) Encrypt(parameters map[string]string) interfaces.AppFun return nil } - transform := transforms.Encryption{ - EncryptionKey: encryptionKey, - InitializationVector: initVector, - SecretPath: secretPath, - SecretName: secretName, - } - switch strings.ToLower(algorithm) { case EncryptAES: + //nolint: staticcheck + transform := transforms.Encryption{ + EncryptionKey: encryptionKey, + InitializationVector: initVector, + SecretPath: secretPath, + SecretName: secretName, + } return transform.EncryptWithAES + case EncryptAES256: + if len(secretPath) > 0 && len(secretName) > 0 { + protector := transforms.AESProtection{ + SecretPath: secretPath, + SecretName: secretName, + } + return protector.Encrypt + } + app.lc.Error("secretPath / secretKey are required for AES 256 encryption") + return nil default: app.lc.Errorf( - "Invalid encryption algorithm '%s'. Must be '%s'", + "Invalid encryption algorithm '%s'. Must be one of '%s', '%s", algorithm, - EncryptAES) + EncryptAES, + EncryptAES256) return nil } } diff --git a/internal/app/configurable_test.go b/internal/app/configurable_test.go index 44e5b58cb..908896ef3 100644 --- a/internal/app/configurable_test.go +++ b/internal/app/configurable_test.go @@ -16,6 +16,7 @@ package app import ( + "github.com/google/uuid" "net/http" "testing" @@ -393,6 +394,8 @@ func TestEncrypt(t *testing.T) { {"Bad - No Key or secrets ", EncryptAES, "", vector, "", "", true}, {"Bad - Missing secretPath", EncryptAES, "", vector, "", secretName, true}, {"Bad - Missing secretName", EncryptAES, "", vector, secretsPath, "", true}, + {"AES256 - Bad - No secrets ", EncryptAES256, "", vector, "", "", true}, + {"AES256 - good - secrets", EncryptAES256, "", vector, uuid.NewString(), uuid.NewString(), false}, } for _, testCase := range tests { diff --git a/internal/etm/LICENSE b/internal/etm/LICENSE new file mode 100644 index 000000000..25c357055 --- /dev/null +++ b/internal/etm/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Coda Hale + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/internal/etm/README.md b/internal/etm/README.md new file mode 100644 index 000000000..4cfaa8de9 --- /dev/null +++ b/internal/etm/README.md @@ -0,0 +1,3 @@ +# etm + +This package contains code retrieved from https://github.com/codahale/etm on 2021-10-28. It implements the crypto.AEAD interface using AES-CBC encryption and sha hashing algorithms. It was stripped of all aead constructions other than `AEAD_AES_256_CBC_HMAC_SHA_512` to fit our usage. \ No newline at end of file diff --git a/internal/etm/etm.go b/internal/etm/etm.go new file mode 100644 index 000000000..659f02fdd --- /dev/null +++ b/internal/etm/etm.go @@ -0,0 +1,206 @@ +// Package etm provides a set of Encrypt-Then-MAC AEAD implementations, which +// combine block ciphers like AES with HMACs. +// +// AEADs +// +// An AEAD (Authenticated Encryption with Associated Data) construction provides +// a unified API for sealing messages in a way which provides both +// confidentiality *and* integrity. +// +// This not only prevents malicious tampering but also eliminates online attacks +// like padding oracle attacks which can allow an attacker to recover plaintexts +// without knowledge of the secret key (e.g., Lucky 13 attack, BEAST attack, +// etc.). +// +// By rejecting ciphertexts which have been modified, these types of attacks are +// eliminated. +// +// Constructions +// +// This package implements one of five proposed standards: +// +// AEAD_AES_256_CBC_HMAC_SHA_512 +// +// Four proposed standards were removed because they aren't used here: +// +// AEAD_AES_128_CBC_HMAC_SHA_256 +// AEAD_AES_192_CBC_HMAC_SHA_384 +// AEAD_AES_256_CBC_HMAC_SHA_384 +// AEAD_AES_128_CBC_HMAC_SHA1 +// +// All constructions combine AES in CBC mode with an HMAC, but vary in the +// degree of security offered and the amount of overhead required. See +// http://tools.ietf.org/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-02 for full +// technical details. +// +// AES-128-CBC-HMAC-SHA-256 +// +// AEAD_AES_128_CBC_HMAC_SHA_256 requires a 32-byte key, provides 128 bits of +// security for both confidentiality and integrity, and adds up to 56 bytes of +// overhead per message. +// +// AES-192-CBC-HMAC-SHA-384 +// +// AEAD_AES_192_CBC_HMAC_SHA_384 requires a 48-byte key, provides 192 bits of +// security for both confidentiality and integrity, and adds up to 64 bytes of +// overhead per message. +// +// AES-256-CBC-HMAC-SHA-384 +// +// AEAD_AES_256_CBC_HMAC_SHA_384 requires a 56-byte key, provides 256 bits of +// security for confidentiality, provides 192 bits of security for integrity, and +// adds up to 64 bytes of overhead per message. +// +// AES-256-CBC-HMAC-SHA-512 +// +// AEAD_AES_256_CBC_HMAC_SHA_512 requires a 64-byte key, provides 256 bits of +// security for both confidentiality and integrity, and adds up to 72 bytes of +// overhead per message. +// +package etm + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha512" + "encoding/binary" + "errors" + "fmt" + "hash" +) + +// NewAES256SHA512 returns an AEAD_AES_256_CBC_HMAC_SHA_512 instance given a +// 64-byte key or an error if the key is the wrong size. +// AEAD_AES_256_CBC_HMAC_SHA_512 combines AES-256 in CBC mode with +// HMAC-SHA-512-256. +func NewAES256SHA512(key []byte) (cipher.AEAD, error) { + return create(etmParams{ + cipherParams: aesCBC, + macAlg: sha512.New, + encKeySize: 32, + macKeySize: 32, + tagSize: 32, + key: key, + }) +} + +type etmParams struct { + cipherParams + encKeySize, macKeySize, tagSize int + + key []byte + macAlg func() hash.Hash +} + +func create(p etmParams) (cipher.AEAD, error) { + l := p.encKeySize + p.macKeySize + if len(p.key) != l { + return nil, fmt.Errorf("etm: key must be %d bytes long", l) + } + encKey, macKey := split(p.key, p.encKeySize, p.macKeySize) + return &etmAEAD{ + etmParams: p, + encKey: encKey, + macKey: macKey, + }, nil +} + +const ( + dataLenSize = 8 +) + +type etmAEAD struct { + etmParams + encKey, macKey []byte +} + +func (aead *etmAEAD) Overhead() int { + return aead.padSize + aead.tagSize + dataLenSize + aead.NonceSize() +} + +func (aead *etmAEAD) NonceSize() int { + return aead.nonceSize +} + +func (aead *etmAEAD) Seal(dst, nonce, plaintext, data []byte) []byte { + b, _ := aead.encAlg(aead.encKey) // guaranteed to work + + c := aead.encrypter(b, nonce) + i := aead.pad(plaintext, aead.blockSize) + s := make([]byte, len(i)) + c.CryptBlocks(s, i) + s = append(nonce, s...) + + t := tag(hmac.New(aead.macAlg, aead.macKey), data, s, aead.tagSize) + + return append(dst, append(s, t...)...) +} + +func (aead *etmAEAD) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { + s := ciphertext[:len(ciphertext)-aead.tagSize] + t := ciphertext[len(ciphertext)-aead.tagSize:] + t2 := tag(hmac.New(aead.macAlg, aead.macKey), data, s, aead.tagSize) + if nonce == nil { + nonce = s[:aead.NonceSize()] + } + + if !hmac.Equal(t, t2) { + return nil, errors.New("message authentication failed") + } + + b, _ := aead.encAlg(aead.encKey) // guaranteed to work + + c := aead.decrypter(b, nonce) + o := make([]byte, len(s)-len(nonce)) + c.CryptBlocks(o, s[len(nonce):]) + + return append(dst, aead.unpad(o, aead.blockSize)...), nil +} + +type cipherParams struct { + nonceSize, blockSize, padSize int + + encAlg func(key []byte) (cipher.Block, error) + encrypter func(cipher.Block, []byte) cipher.BlockMode + decrypter func(cipher.Block, []byte) cipher.BlockMode + pad func([]byte, int) []byte + unpad func([]byte, int) []byte +} + +// AES-CBC-PKCS7 +var aesCBC = cipherParams{ + encAlg: aes.NewCipher, + blockSize: aes.BlockSize, + nonceSize: aes.BlockSize, + encrypter: cipher.NewCBCEncrypter, + decrypter: cipher.NewCBCDecrypter, + padSize: aes.BlockSize, + pad: pkcs7pad, + unpad: pkcs7unpad, +} + +func tag(h hash.Hash, data, s []byte, l int) []byte { + al := make([]byte, dataLenSize) + binary.BigEndian.PutUint64(al, uint64(len(data)*8)) // in bits + h.Write(data) + h.Write(s) + h.Write(al) + return h.Sum(nil)[:l] +} + +func split(key []byte, encKeyLen, macKeyLen int) ([]byte, []byte) { + return key[0:encKeyLen], key[len(key)-macKeyLen:] +} + +func pkcs7pad(b []byte, blockSize int) []byte { + ps := make([]byte, blockSize-(len(b)%blockSize)) + for i := range ps { + ps[i] = byte(len(ps)) + } + return append(b, ps...) +} + +func pkcs7unpad(b []byte, _ int) []byte { + return b[:len(b)-int(b[len(b)-1])] +} diff --git a/internal/etm/etm_test.go b/internal/etm/etm_test.go new file mode 100644 index 000000000..65fb06de1 --- /dev/null +++ b/internal/etm/etm_test.go @@ -0,0 +1,162 @@ +package etm + +import ( + "bytes" + "crypto/cipher" + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "regexp" + "testing" +) + +var _ cipher.AEAD = &etmAEAD{} + +var whitespace = regexp.MustCompile(`[\s]+`) + +func decode(s string) []byte { + b, err := hex.DecodeString(whitespace.ReplaceAllString(s, "")) + if err != nil { + panic(err) + } + return b +} + +func TestOverhead(t *testing.T) { + aead, err := NewAES256SHA512(make([]byte, 64)) + if err != nil { + t.Fatal(err) + } + + expected := 72 + actual := aead.Overhead() + if actual != expected { + t.Errorf("Expected %v but was %v", expected, actual) + } +} + +func TestNonceSize(t *testing.T) { + aead, err := NewAES256SHA512(make([]byte, 64)) + if err != nil { + t.Fatal(err) + } + + expected := 16 + actual := aead.NonceSize() + if actual != expected { + t.Errorf("Expected %v but was %v", expected, actual) + } +} + +func TestBadKeySizes(t *testing.T) { + aead, err := NewAES256SHA512(nil) + if err == nil { + t.Errorf("No error for 256/512, got %v instead", aead) + } +} + +func TestBadMessage(t *testing.T) { + aead, err := NewAES256SHA512(make([]byte, 64)) + if err != nil { + t.Fatal(err) + } + + input := make([]byte, 100) + output := aead.Seal(nil, make([]byte, aead.NonceSize()), input, nil) + output[91] ^= 3 + + b, err := aead.Open(nil, make([]byte, aead.NonceSize()), output, nil) + if err == nil { + t.Errorf("Expected error but got %v", b) + } +} + +func TestAEAD_AES_256_CBC_HMAC_SHA_512(t *testing.T) { + aead, err := NewAES256SHA512(decode(` +20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f +30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f +00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f +10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f +`)) + if err != nil { + t.Fatal(err) + } + + p := decode(` +41 20 63 69 70 68 65 72 20 73 79 73 74 65 6d 20 +6d 75 73 74 20 6e 6f 74 20 62 65 20 72 65 71 75 +69 72 65 64 20 74 6f 20 62 65 20 73 65 63 72 65 +74 2c 20 61 6e 64 20 69 74 20 6d 75 73 74 20 62 +65 20 61 62 6c 65 20 74 6f 20 66 61 6c 6c 20 69 +6e 74 6f 20 74 68 65 20 68 61 6e 64 73 20 6f 66 +20 74 68 65 20 65 6e 65 6d 79 20 77 69 74 68 6f +75 74 20 69 6e 63 6f 6e 76 65 6e 69 65 6e 63 65 +`) + + iv := decode(` +1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04 +`) + + a := decode(` +54 68 65 20 73 65 63 6f 6e 64 20 70 72 69 6e 63 +69 70 6c 65 20 6f 66 20 41 75 67 75 73 74 65 20 +4b 65 72 63 6b 68 6f 66 66 73 +`) + + expected := decode(` +1a f3 8c 2d c2 b9 6f fd d8 66 94 09 23 41 bc 04 +4a ff aa ad b7 8c 31 c5 da 4b 1b 59 0d 10 ff bd +3d d8 d5 d3 02 42 35 26 91 2d a0 37 ec bc c7 bd +82 2c 30 1d d6 7c 37 3b cc b5 84 ad 3e 92 79 c2 +e6 d1 2a 13 74 b7 7f 07 75 53 df 82 94 10 44 6b +36 eb d9 70 66 29 6a e6 42 7e a7 5c 2e 08 46 a1 +1a 09 cc f5 37 0d c8 0b fe cb ad 28 c7 3f 09 b3 +a3 b7 5e 66 2a 25 94 41 0a e4 96 b2 e2 e6 60 9e +31 e6 e0 2c c8 37 f0 53 d2 1f 37 ff 4f 51 95 0b +be 26 38 d0 9d d7 a4 93 09 30 80 6d 07 03 b1 f6 +4d d3 b4 c0 88 a7 f4 5c 21 68 39 64 5b 20 12 bf +2e 62 69 a8 c5 6a 81 6d bc 1b 26 77 61 95 5b c5 +`) + + c := aead.Seal(nil, iv, p, a) + if !bytes.Equal(expected, c) { + t.Errorf("Expected \n%x\n but was \n%x", expected, c) + } + + p2, err := aead.Open(nil, iv, c, a) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(p, p2) { + t.Error("Bad round-trip") + } +} + +func Example() { + key := []byte("yellow submarine was a love song hunt for red october was a film") + plaintext := []byte("this is a secret value") + data := []byte("this is a public value") + + aead, err := NewAES256SHA512(key) + if err != nil { + fmt.Println(err) + return + } + + nonce := make([]byte, aead.NonceSize()) + _, _ = io.ReadFull(rand.Reader, nonce) + + ciphertext := aead.Seal(nil, nonce, plaintext, data) + + secret, err := aead.Open(nil, nil, ciphertext, data) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(string(secret)) + // Output: + // this is a secret value +} diff --git a/pkg/transforms/aesprotection.go b/pkg/transforms/aesprotection.go new file mode 100644 index 000000000..f6ea512a5 --- /dev/null +++ b/pkg/transforms/aesprotection.go @@ -0,0 +1,135 @@ +// +// Copyright (c) 2021 One Track Consulting +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package transforms + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/etm" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + + "github.com/edgexfoundry/go-mod-core-contracts/v2/common" +) + +type AESProtection struct { + SecretPath string + SecretName string + EncryptionKey string +} + +// NewAESProtection creates, initializes and returns a new instance of AESProtection configured +// to retrieve the encryption key from the Secret Store +func NewAESProtection(secretPath string, secretName string) AESProtection { + return AESProtection{ + SecretPath: secretPath, + SecretName: secretName, + } +} + +// Encrypt encrypts a string, []byte, or json.Marshaller type using AES 256 encryption. +// It also signs the data using a SHA512 hash. +// It will return a Base64 encode []byte of the encrypted data. +func (protection AESProtection) Encrypt(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { + if data == nil { + return false, fmt.Errorf("function Encrypt in pipeline '%s': No Data Received", ctx.PipelineId()) + } + + ctx.LoggingClient().Debugf("Encrypting with AES256 in pipeline '%s'", ctx.PipelineId()) + + byteData, err := util.CoerceType(data) + if err != nil { + return false, err + } + + key, err := protection.getKey(ctx) + + if err != nil { + return false, err + } + + if len(key) == 0 { + return false, fmt.Errorf("AES256 encryption key not set in pipeline '%s'", ctx.PipelineId()) + } + + aead, err := etm.NewAES256SHA512(key) + + if err != nil { + return false, err + } + + nonce := make([]byte, aead.NonceSize()) + _, err = rand.Read(nonce) + + if err != nil { + return false, err + } + + dst := make([]byte, 0) + + encrypted := aead.Seal(dst, nonce, byteData, nil) + + clearKey(key) + + encodedData := []byte(base64.StdEncoding.EncodeToString(encrypted)) + + // Set response "content-type" header to "text/plain" + ctx.SetResponseContentType(common.ContentTypeText) + + return true, encodedData +} + +func (protection *AESProtection) getKey(ctx interfaces.AppFunctionContext) ([]byte, error) { + // If using Secret Store for the encryption key + if len(protection.SecretPath) != 0 && len(protection.SecretName) != 0 { + // Note secrets are cached so this call doesn't result in unneeded calls to SecretStore Service and + // the cache is invalidated when StoreSecrets is used. + secretData, err := ctx.GetSecret(protection.SecretPath, protection.SecretName) + if err != nil { + return nil, fmt.Errorf( + "unable to retieve encryption key at secret path=%s and name=%s in pipeline '%s'", + protection.SecretPath, + protection.SecretName, + ctx.PipelineId()) + } + + key, ok := secretData[protection.SecretName] + if !ok { + return nil, fmt.Errorf( + "unable find encryption key in secret data for name=%s in pipeline '%s'", + protection.SecretName, + ctx.PipelineId()) + } + + ctx.LoggingClient().Debugf( + "Using encryption key from Secret Store at path=%s & name=%s in pipeline '%s'", + protection.SecretPath, + protection.SecretName, + ctx.PipelineId()) + + return hex.DecodeString(key) + } + return nil, fmt.Errorf("No key configured") +} + +func clearKey(key []byte) { + for i := range key { + key[i] = 0 + } +} diff --git a/pkg/transforms/aesprotection_test.go b/pkg/transforms/aesprotection_test.go new file mode 100644 index 000000000..8c3ebe5aa --- /dev/null +++ b/pkg/transforms/aesprotection_test.go @@ -0,0 +1,177 @@ +// +// Copyright (c) 2021 One Track Consulting +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package transforms + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "github.com/edgexfoundry/app-functions-sdk-go/v2/internal/etm" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/interfaces/mocks" + "github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util" + "github.com/edgexfoundry/go-mod-core-contracts/v2/clients/logger" + "github.com/edgexfoundry/go-mod-core-contracts/v2/common" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestNewAESProtection(t *testing.T) { + secretPath := uuid.NewString() + secretName := uuid.NewString() + + sut := NewAESProtection(secretPath, secretName) + + assert.Equal(t, secretPath, sut.SecretPath) + assert.Equal(t, secretName, sut.SecretName) +} + +func TestAESProtection_clearKey(t *testing.T) { + key := []byte(uuid.NewString()) + + clearKey(key) + + for _, v := range key { + assert.Equal(t, byte(0), v) + } +} + +func TestAESProtection_getKey(t *testing.T) { + secretPath := uuid.NewString() + secretName := uuid.NewString() + pipelineId := uuid.NewString() + key := "217A24432646294A404E635266556A586E3272357538782F413F442A472D4B6150645367566B59703373367639792442264529482B4D6251655468576D5A7134" + + type fields struct { + SecretPath string + SecretName string + EncryptionKey string + } + tests := []struct { + name string + fields fields + ctxSetup func(ctx *mocks.AppFunctionContext) + wantErr bool + }{ + {name: "no key", wantErr: true}, + { + name: "secret error", + fields: fields{SecretPath: secretPath, SecretName: secretName}, + ctxSetup: func(ctx *mocks.AppFunctionContext) { + ctx.On("GetSecret", secretPath, secretName).Return(nil, fmt.Errorf("secret error")) + }, + wantErr: true, + }, + { + name: "secret not in map", + fields: fields{SecretPath: secretPath, SecretName: secretName}, + ctxSetup: func(ctx *mocks.AppFunctionContext) { + ctx.On("GetSecret", secretPath, secretName).Return(map[string]string{}, nil) + }, + wantErr: true, + }, + { + name: "happy", + fields: fields{SecretPath: secretPath, SecretName: secretName}, + ctxSetup: func(ctx *mocks.AppFunctionContext) { + ctx.On("SetResponsesContentType", common.ContentTypeText).Return() + ctx.On("GetSecret", secretPath, secretName).Return(map[string]string{secretName: key}, nil) + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + aesData := &AESProtection{ + SecretPath: tt.fields.SecretPath, + SecretName: tt.fields.SecretName, + EncryptionKey: tt.fields.EncryptionKey, + } + + ctx := &mocks.AppFunctionContext{} + ctx.On("PipelineId").Return(pipelineId) + ctx.On("LoggingClient").Return(logger.NewMockClient()) + + if tt.ctxSetup != nil { + tt.ctxSetup(ctx) + } + + if k, err := aesData.getKey(ctx); (err != nil) != tt.wantErr { + t.Errorf("getKey() error = %v, wantErr %v", err, tt.wantErr) + + if !tt.wantErr { + assert.Equal(t, key, k) + } + } + }) + } +} + +func TestAESProtection_Encrypt(t *testing.T) { + secretPath := uuid.NewString() + secretName := uuid.NewString() + key := "217A24432646294A404E635266556A586E3272357538782F413F442A472D4B6150645367566B59703373367639792442264529482B4D6251655468576D5A7134" + + ctx := &mocks.AppFunctionContext{} + ctx.On("SetResponseContentType", common.ContentTypeText).Return() + ctx.On("PipelineId").Return("pipeline-id") + ctx.On("LoggingClient").Return(logger.NewMockClient()) + ctx.On("GetSecret", secretPath, secretName).Return(map[string]string{secretName: key}, nil) + + enc := NewAESProtection(secretPath, secretName) + + continuePipeline, encrypted := enc.Encrypt(ctx, []byte(plainString)) + assert.True(t, continuePipeline) + + ebytes, err := util.CoerceType(encrypted) + + require.NoError(t, err) + + //output is base64 encoded + dbytes, err := base64.StdEncoding.DecodeString(string(ebytes)) + + if err != nil { + panic(err) + } + + decrypted := aes256Decrypt(t, dbytes, key) + + assert.Equal(t, plainString, string(decrypted)) +} + +func aes256Decrypt(t *testing.T, dbytes []byte, key string) []byte { + k, err := hex.DecodeString(key) + + if err != nil { + panic(err) + } + + //internally we are leaning heavily on ETM logic + //do not want to re-implement here + etm, err := etm.NewAES256SHA512(k) + + require.NoError(t, err) + + dst := make([]byte, 0) + + res, err := etm.Open(dst, nil, dbytes, nil) + + require.NoError(t, err) + + return res +} diff --git a/pkg/transforms/encryption.go b/pkg/transforms/encryption.go index 313040cab..73c0e52e9 100644 --- a/pkg/transforms/encryption.go +++ b/pkg/transforms/encryption.go @@ -31,6 +31,7 @@ import ( "github.com/edgexfoundry/go-mod-core-contracts/v2/common" ) +// Deprecated: use AESProtection type Encryption struct { SecretPath string SecretName string @@ -39,6 +40,7 @@ type Encryption struct { } // NewEncryption creates, initializes and returns a new instance of Encryption +// Deprecated: use NewAESProtection func NewEncryption(encryptionKey string, initializationVector string) Encryption { return Encryption{ EncryptionKey: encryptionKey, @@ -48,6 +50,7 @@ func NewEncryption(encryptionKey string, initializationVector string) Encryption // NewEncryptionWithSecrets creates, initializes and returns a new instance of Encryption configured // to retrieve the encryption key from the Secret Store +// Deprecated: use NewAESProtection func NewEncryptionWithSecrets(secretPath string, secretName string, initializationVector string) Encryption { return Encryption{ SecretPath: secretPath, @@ -67,11 +70,14 @@ func pkcs5Padding(ciphertext []byte, blockSize int) []byte { // EncryptWithAES encrypts a string, []byte, or json.Marshaller type using AES encryption. // It will return a Base64 encode []byte of the encrypted data. +// Deprecated: use AESProtection.Encrypt func (aesData Encryption) EncryptWithAES(ctx interfaces.AppFunctionContext, data interface{}) (bool, interface{}) { if data == nil { return false, fmt.Errorf("function EncryptWithAES in pipeline '%s': No Data Received", ctx.PipelineId()) } + ctx.LoggingClient().Warnf("EncryptWithAES has been deprecated - please use the new AESProtection.Encrypt in pipeline '%s'", ctx.PipelineId()) + ctx.LoggingClient().Debugf("Encrypting with AES in pipeline '%s'", ctx.PipelineId()) byteData, err := util.CoerceType(data)