CVE-2021-41278: Use of a Broken or Risky Cryptographic Algorithm (CWE-327)
value: Functions SDK for EdgeX is meant to provide all the plumbing necessary for developers to get started in processing/transforming/exporting data out of the EdgeX IoT platform. In affected versions broken encryption in app-functions-sdk “AES” transform in EdgeX Foundry releases prior to Jakarta allows attackers to decrypt messages via unspecified vectors. The app-functions-sdk exports an “aes” transform that user scripts can optionally call to encrypt data in the processing pipeline. No decrypt function is provided. Encryption is not enabled by default, but if used, the level of protection may be less than the user may expects due to a broken implementation. Version v2.1.0 (EdgeX Foundry Jakarta release and later) of app-functions-sdk-go/v2 deprecates the “aes” transform and provides an improved “aes256” transform in its place. The broken implementation will remain in a deprecated state until it is removed in the next EdgeX major release to avoid breakage of existing software that depends on the broken implementation. As the broken transform is a library function that is not invoked by default, users who do not use the AES transform in their processing pipelines are unaffected. Those that are affected are urged to upgrade to the Jakarta EdgeX release and modify processing pipelines to use the new "aes256" transform.
CWE description: The product uses a broken or risky cryptographic algorithm or protocol.
CVSS v3 base score: 5.7
Published: 2021-11-19T00:15Z
Repository: https://github.com/edgexfoundry/app-functions-sdk-go
Language: Go
Files changed: internal/app/configurable.go, internal/app/configurable_test.go, internal/etm/LICENSE, internal/etm/README.md, internal/etm/etm.go, internal/etm/etm_test.go, pkg/transforms/aesprotection.go, pkg/transforms/aesprotection_test.go, pkg/transforms/encryption.go
Commit date: 2021-11-01 16:40:51 -0700
Commit message: 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 <alex.ullrich@gmail.com>
Diff: diff --git a/internal/app/configurable.go b/internal/app/configurable.go
index 9159aa0..11b8e9e 100644
--- a/internal/app/configurable.go
+++ b/internal/app/configurable.go
@@ -73,10 +73,11 @@ const (
 	ResponseContentType = "responsecontenttype"
 	Algorithm           = "algorithm"
 	CompressGZIP        = "gzip"
 	CompressZLIB        = "zlib"
 	EncryptAES          = "aes"
+	EncryptAES256       = "aes256"
 	Mode                = "mode"
 	BatchByCount        = "bycount"
 	BatchByTime         = "bytime"
 	BatchByTimeAndCount = "bytimecount"
 	IsEventData         = "iseventdata"
@@ -315,25 +316,36 @@ func (app *Configurable) Encrypt(parameters map[string]string) interfaces.AppFun
 	if !ok {
 		app.lc.Error("Could not find " + InitVector)
 		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
 	}
 }
 
 // HTTPExport will send data from the previous function to the specified Endpoint via http POST or PUT. If no previous function exists,
diff --git a/internal/app/configurable_test.go b/internal/app/configurable_test.go
index 44e5b58..908896e 100644
--- a/internal/app/configurable_test.go
+++ b/internal/app/configurable_test.go
@@ -14,10 +14,11 @@
 // limitations under the License.
 
 package app
 
 import (
+	"github.com/google/uuid"
 	"net/http"
 	"testing"
 
 	"github.com/edgexfoundry/go-mod-core-contracts/v2/common"
 
@@ -391,10 +392,12 @@ func TestEncrypt(t *testing.T) {
 		{"Bad - No algorithm ", "", key, "", "", "", true},
 		{"Bad - No vector ", EncryptAES, key, "", "", "", true},
 		{"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 {
 		t.Run(testCase.Name, func(t *testing.T) {
 			params := make(map[string]string)
diff --git a/internal/etm/LICENSE b/internal/etm/LICENSE
new file mode 100644
index 0000000..25c3570
--- /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 0000000..4cfaa8d
--- /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 0000000..659f02f
--- /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 0000000..65fb06d
--- /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 0000000..f6ea512
--- /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 0000000..8c3ebe5
--- /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 313040c..73c0e52 100644
--- a/pkg/transforms/encryption.go
+++ b/pkg/transforms/encryption.go
@@ -29,27 +29,30 @@ import (
 	"github.com/edgexfoundry/app-functions-sdk-go/v2/pkg/util"
 
 	"github.com/edgexfoundry/go-mod-core-contracts/v2/common"
 )
 
+// Deprecated: use AESProtection
 type Encryption struct {
 	SecretPath           string
 	SecretName           string
 	EncryptionKey        string
 	InitializationVector string
 }
 
 // NewEncryption creates, initializes and returns a new instance of Encryption
+// Deprecated: use NewAESProtection
 func NewEncryption(encryptionKey string, initializationVector string) Encryption {
 	return Encryption{
 		EncryptionKey:        encryptionKey,
 		InitializationVector: initializationVector,
 	}
 }
 
 // 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,
 		SecretName:           secretName,
 		InitializationVector: initializationVector,
@@ -65,15 +68,18 @@ func pkcs5Padding(ciphertext []byte, blockSize int) []byte {
 	return append(ciphertext, padText...)
 }
 
 // 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)
 	if err != nil {
 		return false, err

Vulnerable code: 	transform := transforms.Encryption{
		EncryptionKey:        encryptionKey,
		InitializationVector: initVector,
		SecretPath:           secretPath,
		SecretName:           secretName,
	}

			"Invalid encryption algorithm '%s'. Must be '%s'",
			EncryptAES)
Fixed code: 	EncryptAES256       = "aes256"
		//nolint: staticcheck
		transform := transforms.Encryption{
			EncryptionKey:        encryptionKey,
			InitializationVector: initVector,
			SecretPath:           secretPath,
			SecretName:           secretName,
		}
	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
			"Invalid encryption algorithm '%s'. Must be one of '%s', '%s",
			EncryptAES,
			EncryptAES256)
	"github.com/google/uuid"
		{"AES256 - Bad - No secrets ", EncryptAES256, "", vector, "", "", true},
		{"AES256 - good - secrets", EncryptAES256, "", vector, uuid.NewString(), uuid.NewString(), false},
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.
# 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.
// 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])]
}
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
}
//
// 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
	}
}
//
// 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
}
// Deprecated: use AESProtection
// Deprecated: use NewAESProtection
// Deprecated: use NewAESProtection
// Deprecated: use AESProtection.Encrypt
	ctx.LoggingClient().Warnf("EncryptWithAES has been deprecated - please use the new AESProtection.Encrypt in pipeline '%s'", ctx.PipelineId())