// 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" ) // 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 } 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 } return passphrases, nil } 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] } } 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") return strings.ToLower(withoutSuffix) } // 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, "_") return withoutAppName[:idx] } // 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 } else { 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], " ", "") 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) 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 } } return secrets, nil }