|
6 | 6 | package crypto
|
7 | 7 |
|
8 | 8 | import (
|
9 |
| - "bytes" |
10 | 9 | "crypto/aes"
|
11 | 10 | "crypto/cipher"
|
12 | 11 | "crypto/rand"
|
13 | 12 | "encoding/base64"
|
| 13 | + "encoding/binary" |
| 14 | + "fmt" |
14 | 15 | "io"
|
15 | 16 |
|
16 | 17 | "github.com/edgexfoundry/edgex-go/internal/pkg/utils/crypto/interfaces"
|
17 |
| - |
| 18 | + bootstrapInterfaces "github.com/edgexfoundry/go-mod-bootstrap/v4/bootstrap/interfaces" |
18 | 19 | "github.com/edgexfoundry/go-mod-core-contracts/v4/errors"
|
| 20 | + "github.com/edgexfoundry/go-mod-secrets/v4/pkg" |
19 | 21 | )
|
20 | 22 |
|
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 | +) |
22 | 45 |
|
23 | 46 | // AESCryptor defined the AES cryptor struct
|
24 | 47 | type AESCryptor struct {
|
25 |
| - key []byte |
| 48 | + key []byte |
| 49 | + keySource string |
| 50 | + nonSecureKey []byte |
26 | 51 | }
|
27 | 52 |
|
28 | 53 | func NewAESCryptor() interfaces.Crypto {
|
29 | 54 | 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) |
31 | 110 | }
|
| 111 | + |
| 112 | + return newKey, nil |
32 | 113 | }
|
33 | 114 |
|
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] |
35 | 117 | func (c *AESCryptor) Encrypt(plaintext string) (string, errors.EdgeX) {
|
36 | 118 | bytePlaintext := []byte(plaintext)
|
| 119 | + |
37 | 120 | block, err := aes.NewCipher(c.key)
|
38 | 121 | if err != nil {
|
39 | 122 | return "", errors.NewCommonEdgeX(errors.KindServerError, "encrypt failed", err)
|
40 | 123 | }
|
41 | 124 |
|
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 | + } |
44 | 129 |
|
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 { |
49 | 136 | return "", errors.NewCommonEdgeX(errors.KindServerError, "encrypt failed", err)
|
50 | 137 | }
|
51 | 138 |
|
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) |
54 | 161 |
|
55 | 162 | return base64.StdEncoding.EncodeToString(ciphertext), nil
|
56 | 163 | }
|
57 | 164 |
|
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] |
59 | 168 | func (c *AESCryptor) Decrypt(ciphertext string) ([]byte, errors.EdgeX) {
|
60 | 169 | decodedCipherText, err := base64.StdEncoding.DecodeString(ciphertext)
|
61 | 170 | if err != nil {
|
62 | 171 | return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err)
|
63 | 172 | }
|
64 | 173 |
|
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) |
66 | 205 | if err != nil {
|
67 | 206 | return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err)
|
68 | 207 | }
|
69 | 208 |
|
70 |
| - if len(decodedCipherText) < aes.BlockSize { |
| 209 | + gcm, err := cipher.NewGCM(block) |
| 210 | + if err != nil { |
71 | 211 | return nil, errors.NewCommonEdgeX(errors.KindServerError, "decrypt failed", err)
|
72 | 212 | }
|
73 | 213 |
|
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 | + |
75 | 232 | 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 | + } |
77 | 239 |
|
78 | 240 | mode := cipher.NewCBCDecrypter(block, iv)
|
79 |
| - mode.CryptBlocks(decodedCipherText, decodedCipherText) |
| 241 | + decryptedText := make([]byte, len(encryptedText)) |
| 242 | + mode.CryptBlocks(decryptedText, encryptedText) |
80 | 243 |
|
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) |
85 | 246 | if e != nil {
|
86 |
| - return nil, errors.NewCommonEdgeXWrapper(err) |
| 247 | + return nil, errors.NewCommonEdgeXWrapper(e) |
87 | 248 | }
|
88 | 249 |
|
89 | 250 | return plaintext, nil
|
90 | 251 | }
|
91 | 252 |
|
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 |
| - |
99 | 253 | // pkcs7Unpad implements the PKCS7 unpadding
|
100 | 254 | func pkcs7Unpad(data []byte) ([]byte, errors.EdgeX) {
|
101 | 255 | length := len(data)
|
|
0 commit comments