Skip to content

Commit 20a28d3

Browse files
authored
feat: Support secure AES key generation and AES-GCM with CBC fallback (#5241)
* feat: Support AES key generation in secure mode Signed-off-by: FelixTing <[email protected]> * feat: Add AES key source in ciphertext - Support `self` (hardcoded key) and `secret-store` (secret store key) key sources - Fix database permission issues when switching from non-secure mode to secure mode - New cipher text format: [formatHeader:4bytes][keySourceLength:4bytes][keySource:string][iv:16bytes][encrypted_data] - formatHeader distinguishes new ciphertext format from legacy format - Improve readability of AES Encrypt and Decrypt methods Limitation: secure mode encrypted data cannot be decrypted in non-secure mode Signed-off-by: FelixTing <[email protected]> * feat: Add AES-GCM encryption mode with CBC backward compatibility Switching from AES-CBC to AES-GCM improves both confidentiality and integrity. AES-GCM is an authenticated encryption mode (AEAD) that provides built-in integrity verification. This prevents common attacks against CBC, such as padding oracle attacks, and ensures that tampering with ciphertext can be detected before decryption. Signed-off-by: FelixTing <[email protected]> --------- Signed-off-by: FelixTing <[email protected]>
1 parent bbf141e commit 20a28d3

File tree

6 files changed

+374
-37
lines changed

6 files changed

+374
-37
lines changed

internal/pkg/utils/crypto/aes.go

Lines changed: 186 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,96 +6,250 @@
66
package crypto
77

88
import (
9-
"bytes"
109
"crypto/aes"
1110
"crypto/cipher"
1211
"crypto/rand"
1312
"encoding/base64"
13+
"encoding/binary"
14+
"fmt"
1415
"io"
1516

1617
"github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces"
17-
18+
bootstrapInterfaces "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/interfaces"
1819
"github.com/edgexfoundry/go-mod-core-contracts/v4/errors"
20+
"github.com/edgexfoundry/go-mod-secrets/v4/pkg"
1921
)
2022

21-
const aesKey = "RO6gGYKocUahpdX15k9gYvbLuSxbKrPz"
23+
const (
24+
aesKey = "RO6gGYKocUahpdX15k9gYvbLuSxbKrPz"
25+
aesSecretName = "aes"
26+
aesKeyName = "key"
27+
28+
keySourceSelf = "self"
29+
keySourceSecretStore = "secret-store"
30+
keySourceLengthSize = 4
31+
formatHeaderSize = 4
32+
// gcmNonceSize defines the size of the nonce (number used once) for AES-GCM mode.
33+
// The nonce ensures that encrypting the same plaintext multiple times produces different ciphertexts.
34+
// 12 bytes is the standard size for optimal performance and security in GCM mode.
35+
gcmNonceSize = 12
36+
// gcmTagSize defines the size of the authentication tag for AES-GCM mode.
37+
// GCM produces a 16-byte authentication tag that provides integrity and authenticity verification.
38+
// This tag is automatically verified during decryption to detect any tampering or corruption.
39+
gcmTagSize = 16
40+
minimumCipherTextSize = formatHeaderSize + keySourceLengthSize + gcmNonceSize + gcmTagSize
41+
// newFormat is a magic number to indicate the new format of the ciphertext.
42+
// The probability of a random legacy ciphertext starting with the same 4 bytes is 1 in 2^32.
43+
newFormat = 0x107ECA18
44+
)
2245

2346
// AESCryptor defined the AES cryptor struct
2447
type AESCryptor struct {
25-
key []byte
48+
key []byte
49+
keySource string
50+
nonSecureKey []byte
2651
}
2752

2853
func NewAESCryptor() interfaces.Crypto {
2954
return &AESCryptor{
30-
key: []byte(aesKey),
55+
key: []byte(aesKey),
56+
keySource: keySourceSelf,
57+
nonSecureKey: []byte(aesKey),
58+
}
59+
}
60+
61+
// NewAESCryptorWithSecretProvider creates a new AES cryptor that uses a key from SecretProvider
62+
// If the key doesn't exist in the Secret Store, it generates a new one and stores it
63+
func NewAESCryptorWithSecretProvider(secretProvider bootstrapInterfaces.SecretProvider) (interfaces.Crypto, error) {
64+
if secretProvider == nil {
65+
return nil, fmt.Errorf("secret provider is nil, cannot create AESCryptor")
66+
}
67+
68+
secrets, err := secretProvider.GetSecret(aesSecretName)
69+
if err == nil {
70+
if aesKeyStr, ok := secrets[aesKeyName]; ok && aesKeyStr != "" {
71+
keyBytes, decodeErr := base64.StdEncoding.DecodeString(aesKeyStr)
72+
if decodeErr == nil {
73+
return &AESCryptor{
74+
key: keyBytes,
75+
keySource: keySourceSecretStore,
76+
nonSecureKey: []byte(aesKey),
77+
}, nil
78+
}
79+
return nil, fmt.Errorf("invalid AES key format in secret store: %w", decodeErr)
80+
}
81+
} else if _, ok := err.(pkg.ErrSecretNameNotFound); !ok {
82+
return nil, fmt.Errorf("failed to get AES key from secret store: %w", err)
83+
}
84+
85+
keyBytes, err := generateAndStoreNewAESKey(secretProvider)
86+
if err != nil {
87+
return nil, err
88+
}
89+
90+
return &AESCryptor{
91+
key: keyBytes,
92+
keySource: keySourceSecretStore,
93+
nonSecureKey: []byte(aesKey),
94+
}, nil
95+
}
96+
97+
func generateAndStoreNewAESKey(secretProvider bootstrapInterfaces.SecretProvider) ([]byte, error) {
98+
newKey := make([]byte, 32) // 256-bit (32 bytes) AES key
99+
if _, err := io.ReadFull(rand.Reader, newKey); err != nil {
100+
return nil, fmt.Errorf("failed to generate AES key: %w", err)
101+
}
102+
103+
keyBase64 := base64.StdEncoding.EncodeToString(newKey)
104+
secrets := map[string]string{
105+
aesKeyName: keyBase64,
106+
}
107+
108+
if err := secretProvider.StoreSecret(aesSecretName, secrets); err != nil {
109+
return nil, fmt.Errorf("failed to store AES key in secret store: %w", err)
31110
}
111+
112+
return newKey, nil
32113
}
33114

34-
// Encrypt encrypts the given plaintext with AES-CBC mode and returns a string in base64 encoding
115+
// Encrypt encrypts the given plaintext with AES-GCM mode
116+
// ciphertext format: [formatHeader:4bytes][keySourceLength:4bytes][keySource:string][nonce:12bytes][encrypted_data]
35117
func (c *AESCryptor) Encrypt(plaintext string) (string, errors.EdgeX) {
36118
bytePlaintext := []byte(plaintext)
119+
37120
block, err := aes.NewCipher(c.key)
38121
if err != nil {
39122
return "", errors.NewCommonEdgeX(errors.KindServerError, "encrypt failed", err)
40123
}
41124

42-
// CBC mode works on blocks so plaintexts may need to be padded to the next whole block
43-
paddedPlaintext := pkcs7Pad(bytePlaintext, block.BlockSize())
125+
gcm, err := cipher.NewGCM(block)
126+
if err != nil {
127+
return "", errors.NewCommonEdgeX(errors.KindServerError, "encrypt failed", err)
128+
}
44129

45-
ciphertext := make([]byte, aes.BlockSize+len(paddedPlaintext))
46-
// attach a random iv ahead of the ciphertext
47-
iv := ciphertext[:aes.BlockSize]
48-
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
130+
keySourceBytes := []byte(c.keySource)
131+
keySourceLength := uint32(len(keySourceBytes))
132+
133+
// generate random nonce for GCM
134+
nonce := make([]byte, gcm.NonceSize())
135+
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
49136
return "", errors.NewCommonEdgeX(errors.KindServerError, "encrypt failed", err)
50137
}
51138

52-
mode := cipher.NewCBCEncrypter(block, iv)
53-
mode.CryptBlocks(ciphertext[aes.BlockSize:], paddedPlaintext)
139+
// encrypt with GCM
140+
encryptedData := gcm.Seal(nil, nonce, bytePlaintext, nil)
141+
142+
// calculate total size: formatHeader + keySourceLength + keySource + nonce + encryptedData
143+
totalSize := formatHeaderSize + keySourceLengthSize + len(keySourceBytes) + len(nonce) + len(encryptedData)
144+
ciphertext := make([]byte, totalSize)
145+
146+
// write format header
147+
binary.BigEndian.PutUint32(ciphertext[:formatHeaderSize], newFormat)
148+
149+
// write keySource length
150+
binary.BigEndian.PutUint32(ciphertext[formatHeaderSize:formatHeaderSize+keySourceLengthSize], keySourceLength)
151+
152+
// write keySource string
153+
copy(ciphertext[formatHeaderSize+keySourceLengthSize:formatHeaderSize+keySourceLengthSize+len(keySourceBytes)], keySourceBytes)
154+
155+
// write nonce
156+
nonceStart := formatHeaderSize + keySourceLengthSize + len(keySourceBytes)
157+
copy(ciphertext[nonceStart:nonceStart+len(nonce)], nonce)
158+
159+
// write encrypted data
160+
copy(ciphertext[nonceStart+len(nonce):], encryptedData)
54161

55162
return base64.StdEncoding.EncodeToString(ciphertext), nil
56163
}
57164

58-
// Decrypt decrypts the given ciphertext with AES-CBC mode and returns the original value as string
165+
// Decrypt decrypts the given ciphertext with AES-GCM mode for new format and AES-CBC mode for legacy format
166+
// ciphertext format for AES-GCM: [formatHeader:4bytes][keySourceLength:4bytes][keySource:string][nonce:12bytes][encrypted_data]
167+
// legacy format for AES-CBC: [iv:16bytes][encrypted_data]
59168
func (c *AESCryptor) Decrypt(ciphertext string) ([]byte, errors.EdgeX) {
60169
decodedCipherText, err := base64.StdEncoding.DecodeString(ciphertext)
61170
if err != nil {
62171
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err)
63172
}
64173

65-
block, err := aes.NewCipher(c.key)
174+
if len(decodedCipherText) > formatHeaderSize {
175+
format := binary.BigEndian.Uint32(decodedCipherText[:formatHeaderSize])
176+
if format == newFormat {
177+
return c.decryptGCM(decodedCipherText)
178+
}
179+
}
180+
181+
return c.decryptCBC(decodedCipherText)
182+
}
183+
184+
// decryptGCM handles decryption for the GCM format
185+
func (c *AESCryptor) decryptGCM(decodedCipherText []byte) ([]byte, errors.EdgeX) {
186+
if len(decodedCipherText) < minimumCipherTextSize {
187+
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed: ciphertext too short for GCM format", nil)
188+
}
189+
190+
keySourceLength := binary.BigEndian.Uint32(decodedCipherText[formatHeaderSize : formatHeaderSize+keySourceLengthSize])
191+
keySourceBytes := decodedCipherText[formatHeaderSize+keySourceLengthSize : formatHeaderSize+keySourceLengthSize+keySourceLength]
192+
keySource := string(keySourceBytes)
193+
194+
var keyToUse []byte
195+
switch keySource {
196+
case keySourceSelf:
197+
keyToUse = c.nonSecureKey
198+
case keySourceSecretStore:
199+
keyToUse = c.key
200+
default:
201+
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed: unsupported keySource", nil)
202+
}
203+
204+
block, err := aes.NewCipher(keyToUse)
66205
if err != nil {
67206
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err)
68207
}
69208

70-
if len(decodedCipherText) < aes.BlockSize {
209+
gcm, err := cipher.NewGCM(block)
210+
if err != nil {
71211
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err)
72212
}
73213

74-
// get the iv from the cipher text
214+
nonceStart := formatHeaderSize + keySourceLengthSize + keySourceLength
215+
nonce := decodedCipherText[nonceStart : nonceStart+gcmNonceSize]
216+
encryptedData := decodedCipherText[nonceStart+gcmNonceSize:]
217+
218+
plaintext, err := gcm.Open(nil, nonce, encryptedData, nil)
219+
if err != nil {
220+
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed: GCM authentication failed", err)
221+
}
222+
223+
return plaintext, nil
224+
}
225+
226+
// decryptCBC handles decryption for the legacy CBC format
227+
func (c *AESCryptor) decryptCBC(decodedCipherText []byte) ([]byte, errors.EdgeX) {
228+
if len(decodedCipherText) < aes.BlockSize {
229+
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed: ciphertext too short for CBC format", nil)
230+
}
231+
75232
iv := decodedCipherText[:aes.BlockSize]
76-
decodedCipherText = decodedCipherText[aes.BlockSize:]
233+
encryptedText := decodedCipherText[aes.BlockSize:]
234+
235+
block, err := aes.NewCipher(c.nonSecureKey)
236+
if err != nil {
237+
return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err)
238+
}
77239

78240
mode := cipher.NewCBCDecrypter(block, iv)
79-
mode.CryptBlocks(decodedCipherText, decodedCipherText)
241+
decryptedText := make([]byte, len(encryptedText))
242+
mode.CryptBlocks(decryptedText, encryptedText)
80243

81-
// If the original plaintext lengths are not a multiple of the block
82-
// size, padding would have to be added when encrypting, which would be
83-
// removed at this point
84-
plaintext, e := pkcs7Unpad(decodedCipherText)
244+
// remove PKCS7 padding
245+
plaintext, e := pkcs7Unpad(decryptedText)
85246
if e != nil {
86-
return nil, errors.NewCommonEdgeXWrapper(err)
247+
return nil, errors.NewCommonEdgeXWrapper(e)
87248
}
88249

89250
return plaintext, nil
90251
}
91252

92-
// pkcs7Pad implements the PKCS7 padding
93-
func pkcs7Pad(data []byte, blockSize int) []byte {
94-
padding := blockSize - (len(data) % blockSize)
95-
padText := bytes.Repeat([]byte{byte(padding)}, padding)
96-
return append(data, padText...)
97-
}
98-
99253
// pkcs7Unpad implements the PKCS7 unpadding
100254
func pkcs7Unpad(data []byte) ([]byte, errors.EdgeX) {
101255
length := len(data)

0 commit comments

Comments
 (0)