Skip to content

Commit 290f8c8

Browse files
authored
Merge pull request #4980 from gofogo/fix-aes-4976
fix(aes-encryption): support plain txt and url safe base64 strings
2 parents fc24607 + 5238a22 commit 290f8c8

File tree

8 files changed

+475
-19
lines changed

8 files changed

+475
-19
lines changed

docs/registry/txt.md

+29-11
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ wildcard domains will have invalid domain syntax and be rejected by most provide
2626

2727
## Encryption
2828

29-
Registry TXT records may contain information, such as the internal ingress name or namespace, considered sensitive, , which attackers could exploit to gather information about your infrastructure.
29+
Registry TXT records may contain information, such as the internal ingress name or namespace, considered sensitive, , which attackers could exploit to gather information about your infrastructure.
3030
By encrypting TXT records, you can protect this information from unauthorized access.
3131

32-
Encryption is enabled by using the `--txt-encrypt-enabled` flag. The 32-byte AES-256-GCM encryption
33-
key must be specified in URL-safe base64 form, using the `--txt-encrypt-aes-key` flag.
32+
Encryption is enabled by setting the `--txt-encrypt-enabled`. The 32-byte AES-256-GCM encryption
33+
key must be specified in URL-safe base64 form (recommended) or be a plain text, using the `--txt-encrypt-aes-key=<key>` flag.
3434

3535
Note that the key used for encryption should be a secure key and properly managed to ensure the security of your TXT records.
3636

@@ -78,14 +78,32 @@ import (
7878
)
7979

8080
func main() {
81-
key := []byte("testtesttesttesttesttesttesttest")
82-
encrypted, _ := endpoint.EncryptText(
83-
"heritage=external-dns,external-dns/owner=example,external-dns/resource=ingress/default/example",
84-
key,
85-
nil,
86-
)
87-
decrypted, _, _ := endpoint.DecryptText(encrypted, key)
88-
fmt.Println(decrypted)
81+
keys := []string{
82+
"ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY=", // safe base64 url encoded 44 bytes and 32 when decoded
83+
"01234567890123456789012345678901", // plain txt 32 bytes
84+
"passphrasewhichneedstobe32bytes!", // plain txt 32 bytes
85+
}
86+
87+
for _, k := range keys {
88+
key := []byte(k)
89+
if len(key) != 32 {
90+
// if key is not a plain txt let's decode
91+
var err error
92+
if key, err = b64.StdEncoding.DecodeString(string(key)); err != nil || len(key) != 32 {
93+
fmt.Errorf("the AES Encryption key must have a length of 32 byte")
94+
}
95+
}
96+
encrypted, _ := endpoint.EncryptText(
97+
"heritage=external-dns,external-dns/owner=example,external-dns/resource=ingress/default/example",
98+
key,
99+
nil,
100+
)
101+
decrypted, _, err := endpoint.DecryptText(encrypted, key)
102+
if err != nil {
103+
fmt.Println("Error decrypting:", err, "for key:", k)
104+
}
105+
fmt.Println(decrypted)
106+
}
89107
}
90108
```
91109

endpoint/crypto_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ limitations under the License.
1717
package endpoint
1818

1919
import (
20+
"encoding/base64"
21+
"io"
2022
"testing"
2123

24+
"crypto/rand"
25+
2226
"github.com/stretchr/testify/require"
2327
)
2428

@@ -56,3 +60,33 @@ func TestEncrypt(t *testing.T) {
5660
t.Error("Decryption of text didn't result in expected plaintext result.")
5761
}
5862
}
63+
64+
func TestGenerateNonceSuccess(t *testing.T) {
65+
nonce, err := GenerateNonce()
66+
require.NoError(t, err)
67+
require.NotEmpty(t, nonce)
68+
69+
// Test nonce length
70+
decodedNonce, err := base64.StdEncoding.DecodeString(string(nonce))
71+
require.NoError(t, err)
72+
require.Equal(t, standardGcmNonceSize, len(decodedNonce))
73+
}
74+
75+
func TestGenerateNonceError(t *testing.T) {
76+
// Save the original rand.Reader
77+
originalRandReader := rand.Reader
78+
defer func() { rand.Reader = originalRandReader }()
79+
80+
// Replace rand.Reader with a faulty reader
81+
rand.Reader = &faultyReader{}
82+
83+
nonce, err := GenerateNonce()
84+
require.Error(t, err)
85+
require.Nil(t, nonce)
86+
}
87+
88+
type faultyReader struct{}
89+
90+
func (f *faultyReader) Read(p []byte) (n int, err error) {
91+
return 0, io.ErrUnexpectedEOF
92+
}

endpoint/labels.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ func NewLabelsFromStringPlain(labelText string) (Labels, error) {
9393
func NewLabelsFromString(labelText string, aesKey []byte) (Labels, error) {
9494
if len(aesKey) != 0 {
9595
decryptedText, encryptionNonce, err := DecryptText(strings.Trim(labelText, "\""), aesKey)
96-
//in case if we have decryption error, just try process original text
97-
//decryption errors should be ignored here, because we can already have plain-text labels in registry
96+
// in case if we have decryption error, just try process original text
97+
// decryption errors should be ignored here, because we can already have plain-text labels in registry
9898
if err == nil {
9999
labels, err := NewLabelsFromStringPlain(decryptedText)
100100
if err == nil {
@@ -152,8 +152,8 @@ func (l Labels) Serialize(withQuotes bool, txtEncryptEnabled bool, aesKey []byte
152152
log.Debugf("Encrypt the serialized text %#v before returning it.", text)
153153
var err error
154154
text, err = EncryptText(text, aesKey, encryptionNonce)
155-
156155
if err != nil {
156+
// if encryption failed, the external-dns will crash
157157
log.Fatalf("Failed to encrypt the text %#v using the encryption key %#v. Got error %#v.", text, aesKey, err)
158158
}
159159

endpoint/labels_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ limitations under the License.
1717
package endpoint
1818

1919
import (
20+
"bytes"
21+
"crypto/rand"
2022
"fmt"
2123
"testing"
2224

25+
log "github.com/sirupsen/logrus"
2326
"github.com/stretchr/testify/suite"
2427
)
2528

@@ -79,6 +82,60 @@ func (suite *LabelsSuite) TestEncryptionNonceReUsage() {
7982
suite.Equal(serialized, suite.fooAsTextEncrypted, "serialized result should be equal")
8083
}
8184

85+
func (suite *LabelsSuite) TestEncryptionKeyChanged() {
86+
foo, err := NewLabelsFromString(suite.fooAsTextEncrypted, suite.aesKey)
87+
suite.NoError(err, "should succeed for valid label text")
88+
89+
serialised := foo.Serialize(false, true, []byte("passphrasewhichneedstobe32bytes!"))
90+
suite.NotEqual(serialised, suite.fooAsTextEncrypted, "serialized result should be equal")
91+
}
92+
93+
func (suite *LabelsSuite) TestEncryptionFailed() {
94+
foo, err := NewLabelsFromString(suite.fooAsTextEncrypted, suite.aesKey)
95+
suite.NoError(err, "should succeed for valid label text")
96+
97+
defer func() { log.StandardLogger().ExitFunc = nil }()
98+
99+
b := new(bytes.Buffer)
100+
101+
var fatalCrash bool
102+
log.StandardLogger().ExitFunc = func(int) { fatalCrash = true }
103+
log.StandardLogger().SetOutput(b)
104+
105+
_ = foo.Serialize(false, true, []byte("wrong-key"))
106+
107+
suite.True(fatalCrash, "should fail if encryption key is wrong")
108+
suite.Contains(b.String(), "Failed to encrypt the text")
109+
}
110+
111+
func (suite *LabelsSuite) TestEncryptionFailedFaultyReader() {
112+
foo, err := NewLabelsFromString(suite.fooAsTextEncrypted, suite.aesKey)
113+
suite.NoError(err, "should succeed for valid label text")
114+
115+
// remove encryption nonce just for simplicity, so that we could regenerate nonce
116+
delete(foo, txtEncryptionNonce)
117+
118+
originalRandReader := rand.Reader
119+
defer func() {
120+
log.StandardLogger().ExitFunc = nil
121+
rand.Reader = originalRandReader
122+
}()
123+
124+
// Replace rand.Reader with a faulty reader
125+
rand.Reader = &faultyReader{}
126+
127+
b := new(bytes.Buffer)
128+
129+
var fatalCrash bool
130+
log.StandardLogger().ExitFunc = func(int) { fatalCrash = true }
131+
log.StandardLogger().SetOutput(b)
132+
133+
_ = foo.Serialize(false, true, suite.aesKey)
134+
135+
suite.True(fatalCrash)
136+
suite.Contains(b.String(), "Failed to generate cryptographic nonce")
137+
}
138+
82139
func (suite *LabelsSuite) TestDeserialize() {
83140
foo, err := NewLabelsFromStringPlain(suite.fooAsText)
84141
suite.NoError(err, "should succeed for valid label text")

registry/dynamodb.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package registry
1818

1919
import (
2020
"context"
21+
b64 "encoding/base64"
2122
"errors"
2223
"fmt"
2324
"strings"
@@ -84,7 +85,10 @@ func NewDynamoDBRegistry(provider provider.Provider, ownerID string, dynamodbAPI
8485
if len(txtEncryptAESKey) == 0 {
8586
txtEncryptAESKey = nil
8687
} else if len(txtEncryptAESKey) != 32 {
87-
return nil, errors.New("the AES Encryption key must have a length of 32 bytes")
88+
var err error
89+
if txtEncryptAESKey, err = b64.StdEncoding.DecodeString(string(txtEncryptAESKey)); err != nil || len(txtEncryptAESKey) != 32 {
90+
return nil, errors.New("the AES Encryption key must be 32 bytes long, in either plain text or base64-encoded format")
91+
}
8892
}
8993
if len(txtPrefix) > 0 && len(txtSuffix) > 0 {
9094
return nil, errors.New("txt-prefix and txt-suffix are mutually exclusive")

registry/dynamodb_test.go

+40-1
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,51 @@ func TestDynamoDBRegistryNew(t *testing.T) {
6161
require.EqualError(t, err, "table cannot be empty")
6262

6363
_, err = NewDynamoDBRegistry(p, "test-owner", api, "test-table", "", "", "", []string{}, []string{}, []byte(";k&l)nUC/33:{?d{3)54+,AD?]SX%yh^x"), time.Hour)
64-
require.EqualError(t, err, "the AES Encryption key must have a length of 32 bytes")
64+
require.EqualError(t, err, "the AES Encryption key must be 32 bytes long, in either plain text or base64-encoded format")
6565

6666
_, err = NewDynamoDBRegistry(p, "test-owner", api, "test-table", "testPrefix", "testSuffix", "", []string{}, []string{}, []byte(""), time.Hour)
6767
require.EqualError(t, err, "txt-prefix and txt-suffix are mutually exclusive")
6868
}
6969

70+
func TestDynamoDBRegistryNew_EncryptionConfig(t *testing.T) {
71+
api, p := newDynamoDBAPIStub(t, nil)
72+
73+
tests := []struct {
74+
encEnabled bool
75+
aesKeyRaw []byte
76+
aesKeySanitized []byte
77+
errorExpected bool
78+
}{
79+
{
80+
encEnabled: true,
81+
aesKeyRaw: []byte("123456789012345678901234567890asdfasdfasdfasdfa12"),
82+
aesKeySanitized: []byte{},
83+
errorExpected: true,
84+
},
85+
{
86+
encEnabled: true,
87+
aesKeyRaw: []byte("passphrasewhichneedstobe32bytes!"),
88+
aesKeySanitized: []byte("passphrasewhichneedstobe32bytes!"),
89+
errorExpected: false,
90+
},
91+
{
92+
encEnabled: true,
93+
aesKeyRaw: []byte("ZPitL0NGVQBZbTD6DwXJzD8RiStSazzYXQsdUowLURY="),
94+
aesKeySanitized: []byte{100, 248, 173, 47, 67, 70, 85, 0, 89, 109, 48, 250, 15, 5, 201, 204, 63, 17, 137, 43, 82, 107, 60, 216, 93, 11, 29, 82, 140, 11, 81, 22},
95+
errorExpected: false,
96+
},
97+
}
98+
for _, test := range tests {
99+
actual, err := NewDynamoDBRegistry(p, "test-owner", api, "test-table", "", "", "", []string{}, []string{}, test.aesKeyRaw, time.Hour)
100+
if test.errorExpected {
101+
require.Error(t, err)
102+
} else {
103+
require.NoError(t, err)
104+
assert.Equal(t, test.aesKeySanitized, actual.txtEncryptAESKey)
105+
}
106+
}
107+
}
108+
70109
func TestDynamoDBRegistryRecordsBadTable(t *testing.T) {
71110
for _, tc := range []struct {
72111
name string

registry/txt.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"strings"
2323
"time"
2424

25+
b64 "encoding/base64"
26+
2527
log "github.com/sirupsen/logrus"
2628

2729
"sigs.k8s.io/external-dns/endpoint"
@@ -63,11 +65,16 @@ func NewTXTRegistry(provider provider.Provider, txtPrefix, txtSuffix, ownerID st
6365
if ownerID == "" {
6466
return nil, errors.New("owner id cannot be empty")
6567
}
68+
6669
if len(txtEncryptAESKey) == 0 {
6770
txtEncryptAESKey = nil
6871
} else if len(txtEncryptAESKey) != 32 {
69-
return nil, errors.New("the AES Encryption key must have a length of 32 bytes")
72+
var err error
73+
if txtEncryptAESKey, err = b64.StdEncoding.DecodeString(string(txtEncryptAESKey)); err != nil || len(txtEncryptAESKey) != 32 {
74+
return nil, errors.New("the AES Encryption key must be 32 bytes long, in either plain text or base64-encoded format")
75+
}
7076
}
77+
7178
if txtEncryptEnabled && txtEncryptAESKey == nil {
7279
return nil, errors.New("the AES Encryption key must be set when TXT record encryption is enabled")
7380
}
@@ -131,7 +138,7 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
131138
}
132139
// We simply assume that TXT records for the registry will always have only one target.
133140
labels, err := endpoint.NewLabelsFromString(record.Targets[0], im.txtEncryptAESKey)
134-
if err == endpoint.ErrInvalidHeritage {
141+
if errors.Is(err, endpoint.ErrInvalidHeritage) {
135142
// if no heritage is found or it is invalid
136143
// case when value of txt record cannot be identified
137144
// record will not be removed as it will have empty owner
@@ -237,7 +244,6 @@ func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpo
237244
txtNew.ProviderSpecific = r.ProviderSpecific
238245
endpoints = append(endpoints, txtNew)
239246
}
240-
241247
return endpoints
242248
}
243249

0 commit comments

Comments
 (0)