// Package secret provides functionality for generating and storing secrets // both in a remote swarm and locally within supported storage such as pass // stores. package secret import ( "fmt" "regexp" "strconv" "strings" "coopcloud.tech/abra/pkg/client" "coopcloud.tech/abra/pkg/config" "github.com/schultz-is/passgen" "github.com/sirupsen/logrus" ) // secretValue represents a parsed `SECRET_FOO=v1 # length=bar` env var config // secret definition. type secretValue struct { Version string Length int } // GeneratePasswords generates passwords. func GeneratePasswords(count, length uint) ([]string, error) { passwords, err := passgen.GeneratePasswords( count, length, passgen.AlphabetDefault, ) if err != nil { return nil, err } logrus.Debugf("generated '%s'", strings.Join(passwords, ", ")) return passwords, nil } // GeneratePassphrases generates human readable and rememberable passphrases. func GeneratePassphrases(count uint) ([]string, error) { passphrases, err := passgen.GeneratePassphrases( count, passgen.PassphraseWordCountDefault, rune('-'), passgen.PassphraseCasingDefault, passgen.WordListDefault, ) if err != nil { return nil, err } logrus.Debugf("generated '%s'", strings.Join(passphrases, ", ")) return passphrases, nil } // ReadSecretEnvVars reads secret env vars from an app env var config. func ReadSecretEnvVars(appEnv config.AppEnv) map[string]string { secretEnvVars := make(map[string]string) for envVar := range appEnv { regex := regexp.MustCompile(`^SECRET.*VERSION.*`) if string(regex.Find([]byte(envVar))) != "" { secretEnvVars[envVar] = appEnv[envVar] } } logrus.Debugf("read '%s' as secrets from '%s'", secretEnvVars, appEnv) return secretEnvVars } // TODO: should probably go in the config/app package? func ParseSecretEnvVarName(secretEnvVar string) string { withoutPrefix := strings.TrimPrefix(secretEnvVar, "SECRET_") withoutSuffix := strings.TrimSuffix(withoutPrefix, "_VERSION") name := strings.ToLower(withoutSuffix) logrus.Debugf("parsed '%s' as name from '%s'", name, secretEnvVar) return name } // TODO: should probably go in the config/app package? func ParseGeneratedSecretName(secret string, appEnv config.App) string { name := fmt.Sprintf("%s_", appEnv.StackName()) withoutAppName := strings.TrimPrefix(secret, name) idx := strings.LastIndex(withoutAppName, "_") parsed := withoutAppName[:idx] logrus.Debugf("parsed '%s' as name from '%s'", parsed, secret) return parsed } // TODO: should probably go in the config/app package? func ParseSecretEnvVarValue(secret string) (secretValue, error) { values := strings.Split(secret, "#") if len(values) == 0 { return secretValue{}, fmt.Errorf("unable to parse '%s'", secret) } if len(values) == 1 { return secretValue{Version: values[0], Length: 0}, nil } split := strings.Split(values[1], "=") parsed := split[len(split)-1] stripped := strings.ReplaceAll(parsed, " ", "") length, err := strconv.Atoi(stripped) if err != nil { return secretValue{}, err } version := strings.ReplaceAll(values[0], " ", "") logrus.Debugf("parsed version '%s' and length '%s' from '%s'", version, length, secret) return secretValue{Version: version, Length: length}, nil } // GenerateSecrets generates secrets locally and sends them to a remote server for storage. func GenerateSecrets(secretEnvVars map[string]string, appName, server string) (map[string]string, error) { secrets := make(map[string]string) ch := make(chan error, len(secretEnvVars)) for secretEnvVar := range secretEnvVars { go func(s string) { secretName := ParseSecretEnvVarName(s) secretValue, err := ParseSecretEnvVarValue(secretEnvVars[s]) if err != nil { ch <- err return } secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secretValue.Version) logrus.Debugf("attempting to generate and store '%s' on '%s'", secretRemoteName, server) if secretValue.Length > 0 { passwords, err := GeneratePasswords(1, uint(secretValue.Length)) if err != nil { ch <- err return } if err := client.StoreSecret(secretRemoteName, passwords[0], server); err != nil { ch <- err return } secrets[secretName] = passwords[0] } else { passphrases, err := GeneratePassphrases(1) if err != nil { ch <- err return } if err := client.StoreSecret(secretRemoteName, passphrases[0], server); err != nil { ch <- err } secrets[secretName] = passphrases[0] } ch <- nil }(secretEnvVar) } for range secretEnvVars { err := <-ch if err != nil { return nil, err } } logrus.Debugf("generated and stored '%s' on '%s'", secrets, server) return secrets, nil }