// 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" "sync" "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 } 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 } 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 } 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 '%v' 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) var mutex sync.Mutex var wg sync.WaitGroup ch := make(chan error, len(secretEnvVars)) for secretEnvVar := range secretEnvVars { wg.Add(1) go func(s string) { defer wg.Done() 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 { if strings.Contains(err.Error(), "AlreadyExists") { logrus.Warnf("%s already exists, moving on...", secretRemoteName) ch <- nil } else { ch <- err } return } mutex.Lock() defer mutex.Unlock() 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 { if strings.Contains(err.Error(), "AlreadyExists") { logrus.Warnf("%s already exists, moving on...", secretRemoteName) ch <- nil } else { ch <- err } return } mutex.Lock() defer mutex.Unlock() secrets[secretName] = passphrases[0] } ch <- nil }(secretEnvVar) } wg.Wait() for range secretEnvVars { err := <-ch if err != nil { return nil, err } } logrus.Debugf("generated and stored %s on %s", secrets, server) return secrets, nil }