Skip to content

Commit

Permalink
Merge 1b7b3c7 into 73bb819
Browse files Browse the repository at this point in the history
  • Loading branch information
HansK-p committed Feb 11, 2020
2 parents 73bb819 + 1b7b3c7 commit 82990bb
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 41 deletions.
124 changes: 124 additions & 0 deletions endpoint/crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
Copyright 2017 The Kubernetes Authors.
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.
*/

/*
This code is based on the article https://www.thepolyglotdeveloper.com/2018/02/encrypt-decrypt-data-golang-application-crypto-packages/
written by Nic Raboy.
Gzip compression/decompression has ben added as there as there are issues with txt records longer than
255 characters and encryption + base64 encoding adds some characters to the resulting text record.
*/

package endpoint

import (
"bytes"
"compress/gzip"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"io"

log "github.com/sirupsen/logrus"
)

// Decompress data
func DecompressData(data []byte) (resData []byte, err error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return data, err
}
defer gz.Close()
var b bytes.Buffer
if _, err = b.ReadFrom(gz); err != nil {
return data, err
}
return b.Bytes(), nil
}

// Compress data
func CompressData(data []byte) (compressedData []byte, err error) {
var b bytes.Buffer
gz, err := gzip.NewWriterLevel(&b, gzip.BestCompression)
if err != nil {
return data, nil
}
// defer gz.Close()
if _, err = gz.Write(data); err != nil {
return data, nil
}

if err = gz.Flush(); err != nil {
return data, nil
}

if err = gz.Close(); err != nil {
return data, nil
}

return b.Bytes(), nil
}

// Encrypt data using the supplied AES key
func EncryptText(text string, aesKey []byte) (string, error) {
block, _ := aes.NewCipher(aesKey)
gcm, err := cipher.NewGCM(block)
if err != nil {
return text, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return text, err
}
data := []byte(text)
data, err = CompressData(data)
if err != nil {
log.Debugf("Failed to compress data based on the text %#v. Got error %#v.", text, err)
}
cipherdata := gcm.Seal(nonce, nonce, data, nil)
return base64.StdEncoding.EncodeToString(cipherdata), nil
}

// Decrypt data using a supplied AES encryption key
func DecryptText(text string, aesKey []byte) (string, error) {
block, err := aes.NewCipher(aesKey)
if err != nil {
return text, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return text, err
}
nonceSize := gcm.NonceSize()
data, err := base64.StdEncoding.DecodeString(text)
if err != nil {
return text, err
}
if len(data) <= nonceSize {
return text, fmt.Errorf("The encoded data from text %#v is shorter than %#v bytes and can't be decoded", text, nonceSize)
}
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
plaindata, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return text, err
}
plaindata, err = DecompressData(plaindata)
if err != nil {
log.Debugf("Failed to decompress data based on the base64 encoded text %#v. Got error %#v.", text, err)
}
return string(plaindata), nil
}
58 changes: 58 additions & 0 deletions endpoint/crypto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
Copyright 2017 The Kubernetes Authors.
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 endpoint

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestEncrypt(t *testing.T) {
// Verify that text encryption and decryption works
aesKey := []byte("s%zF`.*'5`9.AhI2!B,.~hmbs^.*TL?;")
plaintext := "Til pigerene havde han skemtsomme ord, han spøkte med byens børn, han svingede sydvesten og sprang ombord; så heiste han fokken, og hjem han for i solskin, den gamle ørn."
encryptedtext, err := EncryptText(plaintext, aesKey)
require.NoError(t, err)
decryptedtext, err := DecryptText(encryptedtext, aesKey)
require.NoError(t, err)
if plaintext != decryptedtext {
t.Errorf("Original plain text %#v differs from the resulting decrypted text %#v", plaintext, decryptedtext)
}

// Verify that decrypt returns an error and unmodified data if wrong AES encryption key is used
decryptedtext, err = DecryptText(encryptedtext, []byte("s'J!jD`].LC?g&Oa11AgTub,j48ts/96"))
require.Error(t, err)
if decryptedtext != encryptedtext {
t.Error("Data decryption failed, but decrypt still didn't return the original input")
}

// Verify that decrypt returns an error and unmodified data if unencrypted input is is supplied
decryptedtext, err = DecryptText(plaintext, aesKey)
require.Error(t, err)
if plaintext != decryptedtext {
t.Errorf("Data decryption failed, but decrypt still didn't return the original input. Original input %#v, returned data %#v", plaintext, decryptedtext)
}

// Verify that a known encrypted text is decrypted to what is expected
encryptedtext = "TIBnEeYWYd+sffKZ6Wk3Js0pR58TbsFxhXUc+6jzgS4AXCb+NSuKjnlRqp6e5imh5b1hrxWmecrI/QNJzC6o/2U5FZfxY5CjSqhIbCV/gWWkgK1Qd1ohzCO4tzkF5fwGCxSW715NLDUImg6xE4D/hI2pY8wcATnvv4qUw6eR78kDzfn4cwO1IeS/HmFn+ASldPvY6Y3JHCSlbvWTekfRrknHJeLHIsPW5yZVPZCq29xGEe+A29Y="
decryptedtext, err = DecryptText(encryptedtext, aesKey)
require.NoError(t, err)
if decryptedtext != plaintext {
t.Error("Decryption of text didn't result in expected plaintext result.")
}
}
32 changes: 30 additions & 2 deletions endpoint/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package endpoint

import (
log "github.com/sirupsen/logrus"

"errors"
"fmt"
"sort"
Expand Down Expand Up @@ -55,7 +57,7 @@ func NewLabels() Labels {
// NewLabelsFromString constructs endpoints labels from a provided format string
// if heritage set to another value is found then error is returned
// no heritage automatically assumes is not owned by external-dns and returns invalidHeritage error
func NewLabelsFromString(labelText string) (Labels, error) {
func NewLabelsFromStringPlain(labelText string) (Labels, error) {
endpointLabels := map[string]string{}
labelText = strings.Trim(labelText, "\"") // drop quotes
tokens := strings.Split(labelText, ",")
Expand Down Expand Up @@ -85,9 +87,17 @@ func NewLabelsFromString(labelText string) (Labels, error) {
return endpointLabels, nil
}

func NewLabelsFromString(labelText string, aesKey []byte) (Labels, error) {
if aesKey != nil {
labelText = strings.Trim(labelText, "\"") // drop quotes
labelText, _ = DecryptText(labelText, aesKey)
}
return NewLabelsFromStringPlain(labelText)
}

// Serialize transforms endpoints labels into a external-dns recognizable format string
// withQuotes adds additional quotes
func (l Labels) Serialize(withQuotes bool) string {
func (l Labels) SerializePlain(withQuotes bool) string {
var tokens []string
tokens = append(tokens, fmt.Sprintf("heritage=%s", heritage))
var keys []string
Expand All @@ -104,3 +114,21 @@ func (l Labels) Serialize(withQuotes bool) string {
}
return strings.Join(tokens, ",")
}

func (l Labels) Serialize(withQuotes bool, txtEncryptEnabled bool, aesKey []byte) string {
if !txtEncryptEnabled {
return l.SerializePlain(withQuotes)
}
text := l.SerializePlain(false)
log.Debugf("Encrypt the serialized text %#v before returning it.", text)
var err error
text, err = EncryptText(text, aesKey)
if err != nil {
log.Debugf("Failed to encrypt the text %#v using the encryption key %#v. Got error %#v.", text, aesKey, err)
}
if withQuotes {
text = fmt.Sprintf("\"%s\"", text)
}
log.Debugf("Serialized text after encryption is %#v.", text)
return text
}
60 changes: 42 additions & 18 deletions endpoint/labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,63 +25,87 @@ import (

type LabelsSuite struct {
suite.Suite
foo Labels
fooAsText string
fooAsTextWithQuotes string
barText string
barTextAsMap Labels
noHeritageText string
wrongHeritageText string
multipleHeritageText string //considered invalid
aesKey []byte
foo Labels
fooAsText string
fooAsTextWithQuotes string
fooAsTextEncrypted string
fooAsTextWithQuotesEncrypted string
barText string
barTextEncrypted string
barTextAsMap Labels
noHeritageText string
wrongHeritageText string
multipleHeritageText string //considered invalid
}

func (suite *LabelsSuite) SetupTest() {
suite.foo = map[string]string{
"owner": "foo-owner",
"resource": "foo-resource",
}
suite.aesKey = []byte(")K_Fy|?Z.64#UuHm`}[d!GC%WJM_fs{_")
suite.fooAsText = "heritage=external-dns,external-dns/owner=foo-owner,external-dns/resource=foo-resource"
suite.fooAsTextWithQuotes = fmt.Sprintf(`"%s"`, suite.fooAsText)

suite.fooAsTextEncrypted = `+lvP8q9KHJ6BS6O81i2Q6DLNdf2JSKy8j/gbZKviTZlGYj7q+yDoYMgkQ1hPn6urtGllM5bfFMcaaHto52otQtiOYrX8990J3kQqg4s47m3bH3Ejl8RSxSSuWJM3HJtPghQzYg0/LSOsdQ0=`
suite.fooAsTextWithQuotesEncrypted = fmt.Sprintf(`"%s"`, suite.fooAsTextEncrypted)
suite.barTextAsMap = map[string]string{
"owner": "bar-owner",
"resource": "bar-resource",
"new-key": "bar-new-key",
}
suite.barText = "heritage=external-dns,,external-dns/owner=bar-owner,external-dns/resource=bar-resource,external-dns/new-key=bar-new-key,random=stuff,no-equal-sign,," //also has some random gibberish

suite.barTextEncrypted = "yi6vVATlgYN0enXBIupVK2atNUKtajofWMroWtvZjUanFZXlWvqjJPpjmMd91kv86bZj+syQEP0uR3TK6eFVV7oKFh/NxYyh238FjZ+25zlXW9TgbLoMalUNOkhKFdfXkLeeaqJjePB59t+kQBYX+ZEryK652asPs6M+xTIvtg07N7WWZ6SjJujm0RRISg=="
suite.noHeritageText = "external-dns/owner=random-owner"
suite.wrongHeritageText = "heritage=mate,external-dns/owner=random-owner"
suite.multipleHeritageText = "heritage=mate,heritage=external-dns,external-dns/owner=random-owner"
}

func (suite *LabelsSuite) TestSerialize() {
suite.Equal(suite.fooAsText, suite.foo.Serialize(false), "should serializeLabel")
suite.Equal(suite.fooAsTextWithQuotes, suite.foo.Serialize(true), "should serializeLabel")
suite.Equal(suite.fooAsText, suite.foo.SerializePlain(false), "should serializeLabel")
suite.Equal(suite.fooAsTextWithQuotes, suite.foo.SerializePlain(true), "should serializeLabel")
suite.Equal(suite.fooAsText, suite.foo.Serialize(false, false, nil), "should serializeLabel")
suite.Equal(suite.fooAsTextWithQuotes, suite.foo.Serialize(true, false, nil), "should serializeLabel")
suite.Equal(suite.fooAsText, suite.foo.Serialize(false, false, suite.aesKey), "should serializeLabel")
suite.Equal(suite.fooAsTextWithQuotes, suite.foo.Serialize(true, false, suite.aesKey), "should serializeLabel")
suite.NotEqual(suite.fooAsText, suite.foo.Serialize(false, true, suite.aesKey), "should serializeLabel and encrypt")
suite.NotEqual(suite.fooAsTextWithQuotes, suite.foo.Serialize(true, true, suite.aesKey), "should serializeLabel and encrypt")
}

func (suite *LabelsSuite) TestDeserialize() {
foo, err := NewLabelsFromString(suite.fooAsText)
foo, err := NewLabelsFromStringPlain(suite.fooAsText)
suite.NoError(err, "should succeed for valid label text")
suite.Equal(suite.foo, foo, "should reconstruct original label map")

foo, err = NewLabelsFromString(suite.fooAsTextWithQuotes)
foo, err = NewLabelsFromStringPlain(suite.fooAsTextWithQuotes)
suite.NoError(err, "should succeed for valid label text")
suite.Equal(suite.foo, foo, "should reconstruct original label map")

bar, err := NewLabelsFromString(suite.barText)
foo, err = NewLabelsFromString(suite.fooAsTextEncrypted, suite.aesKey)
suite.NoError(err, "should succeed for valid encrypted label text")
suite.Equal(suite.foo, foo, "should reconstruct original label map")

foo, err = NewLabelsFromString(suite.fooAsTextWithQuotesEncrypted, suite.aesKey)
suite.NoError(err, "should succeed for valid encrypted label text")
suite.Equal(suite.foo, foo, "should reconstruct original label map")

bar, err := NewLabelsFromStringPlain(suite.barText)
suite.NoError(err, "should succeed for valid label text")
suite.Equal(suite.barTextAsMap, bar, "should reconstruct original label map")

noHeritage, err := NewLabelsFromString(suite.noHeritageText)
bar, err = NewLabelsFromString(suite.barText, suite.aesKey)
suite.NoError(err, "should succeed for valid encrypted label text")
suite.Equal(suite.barTextAsMap, bar, "should reconstruct original label map")

noHeritage, err := NewLabelsFromStringPlain(suite.noHeritageText)
suite.Equal(ErrInvalidHeritage, err, "should fail if no heritage is found")
suite.Nil(noHeritage, "should return nil")

wrongHeritage, err := NewLabelsFromString(suite.wrongHeritageText)
wrongHeritage, err := NewLabelsFromStringPlain(suite.wrongHeritageText)
suite.Equal(ErrInvalidHeritage, err, "should fail if wrong heritage is found")
suite.Nil(wrongHeritage, "if error should return nil")

multipleHeritage, err := NewLabelsFromString(suite.multipleHeritageText)
multipleHeritage, err := NewLabelsFromStringPlain(suite.multipleHeritageText)
suite.Equal(ErrInvalidHeritage, err, "should fail if multiple heritage is found")
suite.Nil(multipleHeritage, "if error should return nil")
}
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ func main() {
case "noop":
r, err = registry.NewNoopRegistry(p)
case "txt":
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTOwnerID, cfg.TXTCacheInterval)
r, err = registry.NewTXTRegistry(p, cfg.TXTPrefix, cfg.TXTOwnerID, cfg.TXTCacheInterval, cfg.TXTEncryptEnabled, []byte(cfg.TXTEncryptAESKey))
case "aws-sd":
r, err = registry.NewAWSSDRegistry(p.(*provider.AWSSDProvider), cfg.TXTOwnerID)
default:
Expand Down
6 changes: 6 additions & 0 deletions pkg/apis/externaldns/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ type Config struct {
Registry string
TXTOwnerID string
TXTPrefix string
TXTEncryptEnabled bool
TXTEncryptAESKey string
Interval time.Duration
Once bool
DryRun bool
Expand Down Expand Up @@ -199,6 +201,8 @@ var defaultConfig = &Config{
TXTOwnerID: "default",
TXTPrefix: "",
TXTCacheInterval: 0,
TXTEncryptEnabled: false,
TXTEncryptAESKey: "",
Interval: time.Minute,
Once: false,
DryRun: false,
Expand Down Expand Up @@ -378,6 +382,8 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "aws-sd")
app.Flag("txt-owner-id", "When using the TXT registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID)
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional)").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
app.Flag("txt-encrypt-enabled", "When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled)").BoolVar(&cfg.TXTEncryptEnabled)
app.Flag("txt-encrypt-aes-key", "When using the TXT registry, set TXT record decryption and encryption 32 byte aes key (required when --txt-encrypt=true)").Default(defaultConfig.TXTEncryptAESKey).StringVar(&cfg.TXTEncryptAESKey)

// Flags related to the main control loop
app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval)
Expand Down

0 comments on commit 82990bb

Please sign in to comment.