Repository: replicatedhq/kots
Pull request #209 (2019-12-19 20:11:28)

Decrypt passwords when rendering base


Patch:
diff --git a/ffi/main.go b/ffi/main.go
index 0e3b54526..e88930c5d 100644
--- a/ffi/main.go
+++ b/ffi/main.go
@@ -67,6 +67,7 @@ func UpdateCheck(socket, fromArchivePath, namespace string) {
 			LicenseFile:         expectedLicenseFile,
 			Namespace:           namespace,
 			ConfigFile:          filepath.Join(tmpRoot, "upstream", "userdata", "config.yaml"),
+			InstallationFile:    filepath.Join(tmpRoot, "upstream", "userdata", "installation.yaml"),
 			RootDir:             tmpRoot,
 			ExcludeKotsKinds:    true,
 			ExcludeAdminConsole: true,
diff --git a/ffi/templates.go b/ffi/templates.go
index 0b20f2dc8..3ed8d52c0 100644
--- a/ffi/templates.go
+++ b/ffi/templates.go
@@ -12,6 +12,7 @@ import (
 	"github.com/pkg/errors"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
 	kotsconfig "github.com/replicatedhq/kots/pkg/config"
+	"github.com/replicatedhq/kots/pkg/crypto"
 	"github.com/replicatedhq/kots/pkg/template"
 	"k8s.io/client-go/kubernetes/scheme"
 )
@@ -53,11 +54,22 @@ func RenderFile(socket string, filePath string, archivePath string) {
 		builder.AddCtx(template.StaticCtx{})
 
 		// look for config
-		config, values, license, err := findConfig(tmpRoot)
+		config, values, license, installation, err := findConfig(tmpRoot)
 		if err != nil {
 			ffiResult = NewFFIResult(-1).WithError(err)
 			return
 		}
+
+		var cipher *crypto.AESCipher
+		if installation != nil {
+			c, err := crypto.AESCipherFromString(installation.Spec.EncryptionKey)
+			if err != nil {
+				ffiResult = NewFFIResult(-1).WithError(err)
+				return
+			}
+			cipher = c
+		}
+
 		if config != nil {
 			templateContextValues := make(map[string]template.ItemValue)
 
@@ -70,7 +82,7 @@ func RenderFile(socket string, filePath string, archivePath string) {
 				}
 			}
 
-			configCtx, err := builder.NewConfigContext(config.Spec.Groups, templateContextValues)
+			configCtx, err := builder.NewConfigContext(config.Spec.Groups, templateContextValues, cipher)
 			if err != nil {
 				fmt.Printf("failed to create config context %s\n", err.Error())
 				ffiResult = NewFFIResult(1).WithError(err)
@@ -107,13 +119,15 @@ func RenderFile(socket string, filePath string, archivePath string) {
 	}()
 }
 
-func findConfig(archivePath string) (*kotsv1beta1.Config, *kotsv1beta1.ConfigValues, *kotsv1beta1.License, error) {
+func findConfig(archivePath string) (*kotsv1beta1.Config, *kotsv1beta1.ConfigValues, *kotsv1beta1.License, *kotsv1beta1.Installation, error) {
 	var config *kotsv1beta1.Config
 	var values *kotsv1beta1.ConfigValues
 	var license *kotsv1beta1.License
+	var installation *kotsv1beta1.Installation
 
 	err := filepath.Walk(archivePath,
 		func(path string, info os.FileInfo, err error) error {
+			fmt.Printf("%s\n", path)
 			if err != nil {
 				return err
 			}
@@ -139,14 +153,16 @@ func findConfig(archivePath string) (*kotsv1beta1.Config, *kotsv1beta1.ConfigVal
 				values = obj.(*kotsv1beta1.ConfigValues)
 			} else if gvk.Group == "kots.io" && gvk.Version == "v1beta1" && gvk.Kind == "License" {
 				license = obj.(*kotsv1beta1.License)
+			} else if gvk.Group == "kots.io" && gvk.Version == "v1beta1" && gvk.Kind == "Installation" {
+				installation = obj.(*kotsv1beta1.Installation)
 			}
 
 			return nil
 		})
 
 	if err != nil {
-		return nil, nil, nil, errors.Wrap(err, "failed to walk archive dir")
+		return nil, nil, nil, nil, errors.Wrap(err, "failed to walk archive dir")
 	}
 
-	return config, values, license, nil
+	return config, values, license, installation, nil
 }
diff --git a/pkg/base/replicated.go b/pkg/base/replicated.go
index 4dd90c97e..7c83e6e89 100644
--- a/pkg/base/replicated.go
+++ b/pkg/base/replicated.go
@@ -3,6 +3,7 @@ package base
 import (
 	"github.com/pkg/errors"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
+	"github.com/replicatedhq/kots/pkg/crypto"
 	"github.com/replicatedhq/kots/pkg/logger"
 	"github.com/replicatedhq/kots/pkg/template"
 	"github.com/replicatedhq/kots/pkg/upstream"
@@ -10,38 +11,29 @@ import (
 )
 
 func renderReplicated(u *upstream.Upstream, renderOptions *RenderOptions) (*Base, error) {
-	// Find the config for the config groups
-	var config *kotsv1beta1.Config
-	for _, upstreamFile := range u.Files {
-		maybeConfig := tryGetConfigFromFileContent(upstreamFile.Content, renderOptions.Log)
-		if maybeConfig != nil {
-			config = maybeConfig
-		}
-	}
+	config, configValues, license, installation := findConfig(u, renderOptions.Log)
 
-	// Find the values from the context
 	var templateContext map[string]template.ItemValue
-	for _, c := range u.Files {
-		if c.Path == "userdata/config.yaml" {
-			ctx, err := UnmarshalConfigValuesContent(c.Content)
-			if err != nil {
-				renderOptions.Log.Error(err)
-				templateContext = map[string]template.ItemValue{}
-			} else {
-				templateContext = ctx
+	if configValues != nil {
+		ctx := map[string]template.ItemValue{}
+		for k, v := range configValues.Spec.Values {
+			ctx[k] = template.ItemValue{
+				Value:   v.Value,
+				Default: v.Default,
 			}
 		}
+		templateContext = ctx
+	} else {
+		templateContext = map[string]template.ItemValue{}
 	}
 
-	// Find the license
-	var license *kotsv1beta1.License
-	for _, c := range u.Files {
-		if c.Path == "userdata/license.yaml" {
-			maybeLicense := UnmarshalLicenseContent(c.Content, renderOptions.Log)
-			if maybeLicense != nil {
-				license = maybeLicense
-			}
+	var cipher *crypto.AESCipher
+	if installation != nil {
+		c, err := crypto.AESCipherFromString(installation.Spec.EncryptionKey)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to create cipher")
 		}
+		cipher = c
 	}
 
 	baseFiles := []BaseFile{}
@@ -50,7 +42,7 @@ func renderReplicated(u *upstream.Upstream, renderOptions *RenderOptions) (*Base
 	builder.AddCtx(template.StaticCtx{})
 
 	if config != nil {
-		configCtx, err := builder.NewConfigContext(config.Spec.Groups, templateContext)
+		configCtx, err := builder.NewConfigContext(config.Spec.Groups, templateContext, cipher)
 		if err != nil {
 			return nil, errors.Wrap(err, "failed to create config context")
 		}
@@ -138,3 +130,30 @@ func tryGetConfigFromFileContent(content []byte, log *logger.Logger) *kotsv1beta
 
 	return nil
 }
+
+func findConfig(u *upstream.Upstream, log *logger.Logger) (*kotsv1beta1.Config, *kotsv1beta1.ConfigValues, *kotsv1beta1.License, *kotsv1beta1.Installation) {
+	var config *kotsv1beta1.Config
+	var values *kotsv1beta1.ConfigValues
+	var license *kotsv1beta1.License
+	var installation *kotsv1beta1.Installation
+
+	for _, file := range u.Files {
+		decode := scheme.Codecs.UniversalDeserializer().Decode
+		obj, gvk, err := decode(file.Content, nil, nil)
+		if err != nil {
+			log.Debug("%s", err)
+			continue
+		}
+
+		if gvk.Group == "kots.io" && gvk.Version == "v1beta1" && gvk.Kind == "Config" {
+			config = obj.(*kotsv1beta1.Config)
+		} else if gvk.Group == "kots.io" && gvk.Version == "v1beta1" && gvk.Kind == "ConfigValues" {
+			values = obj.(*kotsv1beta1.ConfigValues)
+		} else if gvk.Group == "kots.io" && gvk.Version == "v1beta1" && gvk.Kind == "License" {
+			license = obj.(*kotsv1beta1.License)
+		} else if gvk.Group == "kots.io" && gvk.Version == "v1beta1" && gvk.Kind == "Installation" {
+			installation = obj.(*kotsv1beta1.Installation)
+		}
+	}
+	return config, values, license, installation
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index aa5679c64..c9518613d 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -38,7 +38,7 @@ func TemplateConfig(log *logger.Logger, configPath string, configData string, co
 	}
 
 	// add config context
-	configCtx, err := builder.NewConfigContext(config.Spec.Groups, templateContext)
+	configCtx, err := builder.NewConfigContext(config.Spec.Groups, templateContext, nil)
 	if err != nil {
 		return "", errors.Wrap(err, "failed to create config context")
 	}
diff --git a/pkg/crypto/aes.go b/pkg/crypto/aes.go
index 870ace7c1..f13453501 100644
--- a/pkg/crypto/aes.go
+++ b/pkg/crypto/aes.go
@@ -17,7 +17,7 @@ type AESCipher struct {
 
 const keyLength = 24 // 192 bit
 
-func NewAESCypher() (*AESCipher, error) {
+func NewAESCipher() (*AESCipher, error) {
 	key := make([]byte, keyLength)
 	if _, err := rand.Read(key); err != nil {
 		return nil, errors.Wrap(err, "failed to read key")
diff --git a/pkg/kotsadm/secrets.go b/pkg/kotsadm/secrets.go
index 8533d7443..8f3b1a79a 100644
--- a/pkg/kotsadm/secrets.go
+++ b/pkg/kotsadm/secrets.go
@@ -47,7 +47,7 @@ func getSecretsYAML(deployOptions *DeployOptions) (map[string][]byte, error) {
 	docs["secret-shared-password.yaml"] = sharedPassword.Bytes()
 
 	if deployOptions.APIEncryptionKey == "" {
-		cipher, err := crypto.NewAESCypher()
+		cipher, err := crypto.NewAESCipher()
 		if err != nil {
 			return nil, errors.Wrap(err, "failed to create new API encryption key")
 		}
@@ -279,7 +279,7 @@ func ensureAPIEncryptionSecret(deployOptions *DeployOptions, clientset *kubernet
 	}
 
 	if deployOptions.APIEncryptionKey == "" {
-		cipher, err := crypto.NewAESCypher()
+		cipher, err := crypto.NewAESCipher()
 		if err != nil {
 			return errors.Wrap(err, "failed to create new AES cipher")
 		}
diff --git a/pkg/pull/pull.go b/pkg/pull/pull.go
index b3fbfc61d..4e3a48e4d 100644
--- a/pkg/pull/pull.go
+++ b/pkg/pull/pull.go
@@ -28,6 +28,7 @@ type PullOptions struct {
 	Downstreams         []string
 	LocalPath           string
 	LicenseFile         string
+	InstallationFile    string
 	AirgapRoot          string
 	ConfigFile          string
 	UpdateCursor        string
@@ -132,6 +133,16 @@ func Pull(upstreamURI string, pullOptions PullOptions) (string, error) {
 		}
 		fetchOptions.ConfigValues = config
 	}
+	if pullOptions.InstallationFile != "" {
+		installation, err := parseInstallationFromFile(pullOptions.InstallationFile)
+		if err != nil {
+			return "", errors.Wrap(err, "failed to parse installation from file")
+		}
+		if installation != nil {
+			fetchOptions.EncryptionKey = installation.Spec.EncryptionKey
+		}
+	}
+
 	if pullOptions.AirgapRoot != "" {
 		airgap, err := findAirgapMetaInDir(pullOptions.AirgapRoot)
 		if err != nil {
@@ -424,6 +435,30 @@ func parseConfigValuesFromFile(filename string) (*kotsv1beta1.ConfigValues, erro
 	return config, nil
 }
 
+func parseInstallationFromFile(filename string) (*kotsv1beta1.Installation, error) {
+	contents, err := ioutil.ReadFile(filename)
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, nil
+		}
+		return nil, errors.Wrap(err, "failed to read installation file")
+	}
+
+	decode := scheme.Codecs.UniversalDeserializer().Decode
+	decoded, gvk, err := decode(contents, nil, nil)
+	if err != nil {
+		return nil, errors.Wrap(err, "unable to decode installation file")
+	}
+
+	if gvk.Group != "kots.io" || gvk.Version != "v1beta1" || gvk.Kind != "Installation" {
+		return nil, errors.New("not installation file")
+	}
+
+	installation := decoded.(*kotsv1beta1.Installation)
+
+	return installation, nil
+}
+
 func findAirgapMetaInDir(root string) (*kotsv1beta1.Airgap, error) {
 	files, err := ioutil.ReadDir(root)
 	if err != nil {
diff --git a/pkg/rewrite/rewrite.go b/pkg/rewrite/rewrite.go
index 481bef9df..ed3f8617d 100644
--- a/pkg/rewrite/rewrite.go
+++ b/pkg/rewrite/rewrite.go
@@ -51,6 +51,7 @@ func Rewrite(rewriteOptions RewriteOptions) error {
 		LocalPath:           rewriteOptions.UpstreamPath,
 		CurrentCursor:       rewriteOptions.Installation.Spec.UpdateCursor,
 		CurrentVersionLabel: rewriteOptions.Installation.Spec.VersionLabel,
+		EncryptionKey:       rewriteOptions.Installation.Spec.EncryptionKey,
 		License:             rewriteOptions.License,
 	}
 
diff --git a/pkg/template/config_context.go b/pkg/template/config_context.go
index dfd7fa3f5..590407884 100644
--- a/pkg/template/config_context.go
+++ b/pkg/template/config_context.go
@@ -7,9 +7,10 @@ import (
 
 	"github.com/pkg/errors"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
+	"github.com/replicatedhq/kots/pkg/crypto"
 )
 
-func (b *Builder) NewConfigContext(configGroups []kotsv1beta1.ConfigGroup, templateContext map[string]ItemValue) (*ConfigCtx, error) {
+func (b *Builder) NewConfigContext(configGroups []kotsv1beta1.ConfigGroup, templateContext map[string]ItemValue, cipher *crypto.AESCipher) (*ConfigCtx, error) {
 	configCtx := &ConfigCtx{
 		ItemValues: templateContext,
 	}
@@ -36,6 +37,13 @@ func (b *Builder) NewConfigContext(configGroups []kotsv1beta1.ConfigGroup, templ
 				}
 			}
 
+			if configItem.Type == "password" && itemValue.HasValue() {
+				// FIXME: this temporarily ignores errors and falls back on old behavior
+				val, err := decrypt(itemValue.ValueStr(), cipher)
+				if err == nil {
+					itemValue.Value = val
+				}
+			}
 			configCtx.ItemValues[configItem.Name] = itemValue
 		}
 	}
@@ -148,3 +156,21 @@ func (ctx ConfigCtx) getConfigOptionValue(itemName string) (string, error) {
 
 	return val.DefaultStr(), nil
 }
+
+func decrypt(input string, cipher *crypto.AESCipher) (string, error) {
+	if cipher == nil {
+		return "", errors.New("cipher not defined")
+	}
+
+	decoded, err := base64.StdEncoding.DecodeString(input)
+	if err != nil {
+		return "", errors.Wrap(err, "failed to base64 decode")
+	}
+
+	decrypted, err := cipher.Decrypt(decoded)
+	if err != nil {
+		return "", errors.Wrap(err, "failed to decrypt")
+	}
+
+	return string(decrypted), nil
+}
diff --git a/pkg/upstream/fetch.go b/pkg/upstream/fetch.go
index 63b8d5c30..260b0557d 100644
--- a/pkg/upstream/fetch.go
+++ b/pkg/upstream/fetch.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/pkg/errors"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
+	"github.com/replicatedhq/kots/pkg/crypto"
 	"github.com/replicatedhq/kots/pkg/util"
 )
 
@@ -18,6 +19,7 @@ type FetchOptions struct {
 	License             *kotsv1beta1.License
 	ConfigValues        *kotsv1beta1.ConfigValues
 	Airgap              *kotsv1beta1.Airgap
+	EncryptionKey       string
 	CurrentCursor       string
 	CurrentVersionLabel string
 }
@@ -36,6 +38,15 @@ func downloadUpstream(upstreamURI string, fetchOptions *FetchOptions) (*Upstream
 		return readFilesFromPath(upstreamURI)
 	}
 
+	var cipher *crypto.AESCipher
+	if fetchOptions.EncryptionKey != "" {
+		c, err := crypto.AESCipherFromString(fetchOptions.EncryptionKey)
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to create cipher")
+		}
+		cipher = c
+	}
+
 	u, err := url.ParseRequestURI(upstreamURI)
 	if err != nil {
 		return nil, errors.Wrap(err, "parse request uri failed")
@@ -44,7 +55,7 @@ func downloadUpstream(upstreamURI string, fetchOptions *FetchOptions) (*Upstream
 		return downloadHelm(u, fetchOptions.HelmRepoURI)
 	}
 	if u.Scheme == "replicated" {
-		return downloadReplicated(u, fetchOptions.LocalPath, fetchOptions.RootDir, fetchOptions.UseAppDir, fetchOptions.License, fetchOptions.ConfigValues, fetchOptions.CurrentCursor, pickVersionLabel(fetchOptions))
+		return downloadReplicated(u, fetchOptions.LocalPath, fetchOptions.RootDir, fetchOptions.UseAppDir, fetchOptions.License, fetchOptions.ConfigValues, fetchOptions.CurrentCursor, pickVersionLabel(fetchOptions), cipher)
 	}
 	if u.Scheme == "git" {
 		return downloadGit(upstreamURI)
diff --git a/pkg/upstream/replicated.go b/pkg/upstream/replicated.go
index d22941c87..2c7443f61 100644
--- a/pkg/upstream/replicated.go
+++ b/pkg/upstream/replicated.go
@@ -21,6 +21,7 @@ import (
 	dockerref "github.com/containers/image/docker/reference"
 	"github.com/pkg/errors"
 	kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
+	"github.com/replicatedhq/kots/pkg/crypto"
 	"github.com/replicatedhq/kots/pkg/docker/registry"
 	"github.com/replicatedhq/kots/pkg/image"
 	"github.com/replicatedhq/kots/pkg/k8sdoc"
@@ -108,7 +109,7 @@ func getUpdatesReplicated(u *url.URL, localPath string, currentCursor, versionLa
 	return updates, nil
 }
 
-func downloadReplicated(u *url.URL, localPath string, rootDir string, useAppDir bool, license *kotsv1beta1.License, existingConfigValues *kotsv1beta1.ConfigValues, updateCursor, versionLabel string) (*Upstream, error) {
+func downloadReplicated(u *url.URL, localPath string, rootDir string, useAppDir bool, license *kotsv1beta1.License, existingConfigValues *kotsv1beta1.ConfigValues, updateCursor, versionLabel string, cipher *crypto.AESCipher) (*Upstream, error) {
 	var release *Release
 
 	if localPath != "" {
@@ -172,7 +173,7 @@ func downloadReplicated(u *url.URL, localPath string, rootDir string, useAppDir
 	if config != nil || existingConfigValues != nil {
 		// If config existed and was removed from the app,
 		// values will be carried over to the new version anyway.
-		configValues, err := createConfigValues(application.Name, config, existingConfigValues)
+		configValues, err := createConfigValues(application.Name, config, existingConfigValues, cipher)
 		if err != nil {
 			return nil, errors.Wrap(err, "failed to create empty config values")
 		}
@@ -439,7 +440,7 @@ func mustMarshalConfigValues(configValues *kotsv1beta1.ConfigValues) []byte {
 	return b.Bytes()
 }
 
-func createConfigValues(applicationName string, config *kotsv1beta1.Config, existingConfigValues *kotsv1beta1.ConfigValues) (*kotsv1beta1.ConfigValues, error) {
+func createConfigValues(applicationName string, config *kotsv1beta1.Config, existingConfigValues *kotsv1beta1.ConfigValues, cipher *crypto.AESCipher) (*kotsv1beta1.ConfigValues, error) {
 	templateContextValues := make(map[string]template.ItemValue)
 
 	var newValues kotsv1beta1.ConfigValuesSpec
@@ -475,7 +476,7 @@ func createConfigValues(applicationName string, config *kotsv1beta1.Config, exis
 	builder := template.Builder{}
 	builder.AddCtx(template.StaticCtx{})
 
-	configCtx, err := builder.NewConfigContext(config.Spec.Groups, templateContextValues)
+	configCtx, err := builder.NewConfigContext(config.Spec.Groups, templateContextValues, cipher)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to create config context")
 	}
diff --git a/pkg/upstream/write.go b/pkg/upstream/write.go
index 7e4d969ce..d7fe8cf95 100644
--- a/pkg/upstream/write.go
+++ b/pkg/upstream/write.go
@@ -150,7 +150,7 @@ func (u *Upstream) GetBaseDir(options WriteOptions) string {
 
 func getEncryptionKey(previousInstallationContent []byte) (string, error) {
 	if previousInstallationContent == nil {
-		cipher, err := crypto.NewAESCypher()
+		cipher, err := crypto.NewAESCipher()
 		if err != nil {
 			return "", errors.Wrap(err, "failed to create new AES cipher")
 		}


Test patch:
diff --git a/pkg/upstream/replicated_test.go b/pkg/upstream/replicated_test.go
index 29e7997a8..9484ff045 100644
--- a/pkg/upstream/replicated_test.go
+++ b/pkg/upstream/replicated_test.go
@@ -232,13 +232,13 @@ func Test_createConfigValues(t *testing.T) {
 			Default: "default_4",
 		},
 	}
-	values1, err := createConfigValues(applicationName, config, nil)
+	values1, err := createConfigValues(applicationName, config, nil, nil)
 	req.NoError(err)
 	assert.Equal(t, expected1, values1.Spec.Values)
 
 	// Like an app without a config, should have exact same values
 	expected2 := configValues.Spec.Values
-	values2, err := createConfigValues(applicationName, nil, configValues)
+	values2, err := createConfigValues(applicationName, nil, configValues, nil)
 	req.NoError(err)
 	assert.Equal(t, expected2, values2.Spec.Values)
 
@@ -259,7 +259,7 @@ func Test_createConfigValues(t *testing.T) {
 			Default: "default_4",
 		},
 	}
-	values3, err := createConfigValues(applicationName, config, configValues)
+	values3, err := createConfigValues(applicationName, config, configValues, nil)
 	req.NoError(err)
 	assert.Equal(t, expected3, values3.Spec.Values)
 }