diff --git a/cmd/internal/exec.go b/cmd/internal/exec.go index 69231d0b..c8dc90e9 100644 --- a/cmd/internal/exec.go +++ b/cmd/internal/exec.go @@ -28,8 +28,6 @@ import ( "github.com/flowexec/flow/internal/runner/serial" "github.com/flowexec/flow/internal/services/store" "github.com/flowexec/flow/internal/utils/env" - "github.com/flowexec/flow/internal/vault" - vaultV2 "github.com/flowexec/flow/internal/vault/v2" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" ) @@ -169,9 +167,6 @@ func execFunc(ctx *context.Context, cmd *cobra.Command, verb executable.Verb, ar } } - if ctx.Config.CurrentVault == nil || *ctx.Config.CurrentVault == vaultV2.LegacyVaultReservedName { - setAuthEnv(ctx, cmd, e, false) - } startTime := time.Now() eng := engine.NewExecEngine() @@ -236,103 +231,6 @@ func runByRef(ctx *context.Context, cmd *cobra.Command, argsStr string) error { return nil } -func setAuthEnv(ctx *context.Context, _ *cobra.Command, executable *executable.Executable, force bool) { - if authRequired(ctx, executable) || force { - form, err := views.NewForm( - io.Theme(ctx.Config.Theme.String()), - ctx.StdIn(), - ctx.StdOut(), - &views.FormField{ - Key: vault.EncryptionKeyEnvVar, - Title: "Enter vault encryption key", - Type: views.PromptTypeMasked, - }) - if err != nil { - logger.Log().FatalErr(err) - } - if err := form.Run(ctx); err != nil { - logger.Log().FatalErr(err) - } - val := form.FindByKey(vault.EncryptionKeyEnvVar).Value() - if val == "" { - logger.Log().FatalErr(fmt.Errorf("vault encryption key required")) - } - if err := os.Setenv(vault.EncryptionKeyEnvVar, val); err != nil { - logger.Log().FatalErr(fmt.Errorf("failed to set vault encryption key\n%w", err)) - } - } -} - -// TODO: refactor this function to simplify the logic -// -//nolint:all -func authRequired(ctx *context.Context, rootExec *executable.Executable) bool { - if os.Getenv(vault.EncryptionKeyEnvVar) != "" { - return false - } - switch { - case rootExec.Exec != nil: - for _, param := range rootExec.Exec.Params { - if param.SecretRef != "" { - return true - } - } - case rootExec.Launch != nil: - for _, param := range rootExec.Launch.Params { - if param.SecretRef != "" { - return true - } - } - case rootExec.Request != nil: - for _, param := range rootExec.Request.Params { - if param.SecretRef != "" { - return true - } - } - case rootExec.Render != nil: - for _, param := range rootExec.Render.Params { - if param.SecretRef != "" { - return true - } - } - case rootExec.Serial != nil: - for _, param := range rootExec.Serial.Params { - if param.SecretRef != "" { - return true - } - } - for _, e := range rootExec.Serial.Execs { - if e.Ref != "" { - childExec, err := ctx.ExecutableCache.GetExecutableByRef(e.Ref) - if err != nil { - continue - } - if authRequired(ctx, childExec) { - return true - } - } - } - case rootExec.Parallel != nil: - for _, param := range rootExec.Parallel.Params { - if param.SecretRef != "" { - return true - } - } - for _, e := range rootExec.Parallel.Execs { - if e.Ref != "" { - childExec, err := ctx.ExecutableCache.GetExecutableByRef(e.Ref) - if err != nil { - continue - } - if authRequired(ctx, childExec) { - return true - } - } - } - } - return false -} - //nolint:gocognit func pendingFormFields( ctx *context.Context, rootExec *executable.Executable, envMap map[string]string, diff --git a/cmd/internal/secret.go b/cmd/internal/secret.go index dfb73ed4..1aa53279 100644 --- a/cmd/internal/secret.go +++ b/cmd/internal/secret.go @@ -15,12 +15,10 @@ import ( "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/io" "github.com/flowexec/flow/internal/io/secret" - secretV2 "github.com/flowexec/flow/internal/io/secret/v2" "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/utils" envUtils "github.com/flowexec/flow/internal/utils/env" "github.com/flowexec/flow/internal/vault" - vaultV2 "github.com/flowexec/flow/internal/vault/v2" "github.com/flowexec/flow/types/config" ) @@ -72,22 +70,14 @@ func removeSecretFunc(ctx *context.Context, _ *cobra.Command, args []string) { return } - if currentVault(ctx.Config) == vaultV2.LegacyVaultReservedName { - logger.Log().Warnf("Using deprecated vault. Consider creating a new vault with 'flow vault create' command.") - v := vault.NewVault() - if err = v.DeleteSecret(reference); err != nil { - logger.Log().FatalErr(err) - } - } else { - _, v, err := vaultV2.VaultFromName(currentVault(ctx.Config)) - defer v.Close() + _, v, err := vault.VaultFromName(currentVault(ctx.Config)) + defer v.Close() - if err != nil { - logger.Log().FatalErr(err) - } - if err = v.DeleteSecret(reference); err != nil { - logger.Log().FatalErr(err) - } + if err != nil { + logger.Log().FatalErr(err) + } + if err = v.DeleteSecret(reference); err != nil { + logger.Log().FatalErr(err) } logger.Log().PlainTextSuccess(fmt.Sprintf("Secret '%s' deleted from vault", reference)) @@ -148,28 +138,15 @@ func setSecretFunc(ctx *context.Context, cmd *cobra.Command, args []string) { value = strings.Join(args[1:], " ") } - sv := vault.SecretValue(value) vaultName := currentVault(ctx.Config) - if vaultName == vaultV2.LegacyVaultReservedName { - logger.Log().Warnf( - "Using deprecated vault '%s'. Consider creating a new vault with 'flow vault create' command.", - vaultName, - ) - v := vault.NewVault() - err := v.SetSecret(reference, sv) - if err != nil { - logger.Log().FatalErr(err) - } - } else { - _, v, err := vaultV2.VaultFromName(vaultName) - defer v.Close() + _, v, err := vault.VaultFromName(vaultName) + defer v.Close() - if err != nil { - logger.Log().FatalErr(err) - } - if err = v.SetSecret(reference, vaultV2.NewSecretValue([]byte(value))); err != nil { - logger.Log().FatalErr(err) - } + if err != nil { + logger.Log().FatalErr(err) + } + if err = v.SetSecret(reference, vault.NewSecretValue([]byte(value))); err != nil { + logger.Log().FatalErr(err) } logger.Log().PlainTextSuccess(fmt.Sprintf("Secret %s set in vault", reference)) @@ -194,45 +171,29 @@ func listSecretFunc(ctx *context.Context, cmd *cobra.Command, _ []string) { asPlainText := flags.ValueFor[bool](cmd, *flags.OutputSecretAsPlainTextFlag, false) outputFormat := flags.ValueFor[string](cmd, *flags.OutputFormatFlag, false) - //nolint:nestif - if currentVault(ctx.Config) == vaultV2.LegacyVaultReservedName { - v := vault.NewVault() - secrets, err := v.GetAllSecrets() - if err != nil { - logger.Log().FatalErr(err) - } - - interactiveUI := TUIEnabled(ctx, cmd) - if interactiveUI { - secret.LoadSecretListView(ctx, asPlainText) - } else { - secret.PrintSecrets(ctx, secrets, outputFormat, asPlainText) - } - } else { - name := currentVault(ctx.Config) - interactiveUI := TUIEnabled(ctx, cmd) + name := currentVault(ctx.Config) + interactiveUI := TUIEnabled(ctx, cmd) - _, v, err := vaultV2.VaultFromName(name) - defer func() { - // Don't close the vault prematurely if we're in interactive mode - go func() { - if interactiveUI { - ctx.TUIContainer.WaitForExit() - } - _ = v.Close() - }() + _, v, err := vault.VaultFromName(name) + defer func() { + // Don't close the vault prematurely if we're in interactive mode + go func() { + if interactiveUI { + ctx.TUIContainer.WaitForExit() + } + _ = v.Close() }() + }() - if err != nil { - logger.Log().FatalErr(err) - } + if err != nil { + logger.Log().FatalErr(err) + } - if interactiveUI { - view := secretV2.NewSecretListView(ctx, v, asPlainText) - SetView(ctx, cmd, view) - } else { - secretV2.PrintSecrets(ctx, name, v, outputFormat, asPlainText) - } + if interactiveUI { + view := secret.NewSecretListView(ctx, v, asPlainText) + SetView(ctx, cmd, view) + } else { + secret.PrintSecrets(ctx, name, v, outputFormat, asPlainText) } } @@ -254,65 +215,41 @@ func getSecretFunc(ctx *context.Context, cmd *cobra.Command, args []string) { asPlainText := flags.ValueFor[bool](cmd, *flags.OutputSecretAsPlainTextFlag, false) copyValue := flags.ValueFor[bool](cmd, *flags.CopyFlag, false) - //nolint:nestif - if currentVault(ctx.Config) == vaultV2.LegacyVaultReservedName { - logger.Log().Warnf("Using deprecated vault. Consider creating a new vault with 'flow vault create' command.") - v := vault.NewVault() - s, err := v.GetSecret(reference) - if err != nil { - logger.Log().FatalErr(err) - } + rVault, key, err := vault.RefToParts(vault.SecretRef(reference)) + if err != nil { + logger.Log().FatalErr(err) + } + if rVault == "" { + rVault = currentVault(ctx.Config) + } + _, v, err := vault.VaultFromName(rVault) + defer v.Close() - if asPlainText { - logger.Log().PlainTextInfo(s.PlainTextString()) - } else { - logger.Log().PlainTextInfo(s.String()) - } + if err != nil { + logger.Log().FatalErr(err) + } + s, err := v.GetSecret(key) + if err != nil { + logger.Log().FatalErr(err) + } - if copyValue { - if err := clipboard.WriteAll(s.PlainTextString()); err != nil { - logger.Log().Error(err, "\nunable to copy secret value to clipboard") - } else { - logger.Log().PlainTextSuccess("\ncopied secret value to clipboard") - } - } + if asPlainText { + logger.Log().PlainTextInfo(s.PlainTextString()) } else { - rVault, key, err := vaultV2.RefToParts(vaultV2.SecretRef(reference)) - if err != nil { - logger.Log().FatalErr(err) - } - if rVault == "" { - rVault = currentVault(ctx.Config) - } - _, v, err := vaultV2.VaultFromName(rVault) - defer v.Close() - - if err != nil { - logger.Log().FatalErr(err) - } - s, err := v.GetSecret(key) - if err != nil { - logger.Log().FatalErr(err) - } - - if asPlainText { - logger.Log().PlainTextInfo(s.PlainTextString()) + logger.Log().PlainTextInfo(s.String()) + } + if copyValue { + if err := clipboard.WriteAll(s.PlainTextString()); err != nil { + logger.Log().Error(err, "\nunable to copy secret value to clipboard") } else { - logger.Log().PlainTextInfo(s.String()) - } - if copyValue { - if err := clipboard.WriteAll(s.PlainTextString()); err != nil { - logger.Log().Error(err, "\nunable to copy secret value to clipboard") - } else { - logger.Log().PlainTextSuccess("\ncopied secret value to clipboard") - } + logger.Log().PlainTextSuccess("\ncopied secret value to clipboard") } } } func currentVault(cfg *config.Config) string { - if cfg.CurrentVault == nil || *cfg.CurrentVault == "" { - return vaultV2.LegacyVaultReservedName + if cfg.CurrentVault == nil { + return "" } return *cfg.CurrentVault } diff --git a/cmd/internal/vault.go b/cmd/internal/vault.go index 09b006ec..7a93dea2 100644 --- a/cmd/internal/vault.go +++ b/cmd/internal/vault.go @@ -18,7 +18,6 @@ import ( "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/utils" "github.com/flowexec/flow/internal/vault" - vaultV2 "github.com/flowexec/flow/internal/vault/v2" "github.com/flowexec/flow/types/config" ) @@ -35,7 +34,6 @@ func RegisterVaultCmd(ctx *context.Context, rootCmd *cobra.Command) { registerSwitchVaultCmd(ctx, vaultCmd) registerRemoveVaultCmd(ctx, vaultCmd) registerEditVaultCmd(ctx, vaultCmd) - registerMigrateVaultCmd(ctx, vaultCmd) // TODO: add command for testing vault connectivity rootCmd.AddCommand(vaultCmd) } @@ -48,9 +46,9 @@ func registerCreateVaultCmd(ctx *context.Context, vaultCmd *cobra.Command) { Args: cobra.ExactArgs(1), PreRun: func(cmd *cobra.Command, args []string) { vaultName := args[0] - if vaultName == vaultV2.LegacyVaultReservedName || vaultName == vaultV2.DemoVaultReservedName { + if vaultName == vault.DemoVaultReservedName { logger.Log().Fatalf("create is unsupported for the reserved vaults") - } else if err := vault.ValidateReference(vaultName); err != nil { + } else if err := vault.ValidateIdentifier(vaultName); err != nil { logger.Log().Fatalf("invalid vault name '%s': %v", vaultName, err) } @@ -84,25 +82,25 @@ func createVaultFunc(ctx *context.Context, cmd *cobra.Command, args []string) { switch strings.ToLower(vaultType) { case "unencrypted": - vaultV2.NewUnencryptedVault(vaultName, vaultPath) + vault.NewUnencryptedVault(vaultName, vaultPath) case "aes256": keyEnv := flags.ValueFor[string](cmd, *flags.VaultKeyEnvFlag, false) keyFile := flags.ValueFor[string](cmd, *flags.VaultKeyFileFlag, false) logLevel := flags.ValueFor[string](cmd, *flags.LogLevel, false) - vaultV2.NewAES256Vault(vaultName, vaultPath, keyEnv, keyFile, logLevel) + vault.NewAES256Vault(vaultName, vaultPath, keyEnv, keyFile, logLevel) case "age": recipients := flags.ValueFor[string](cmd, *flags.VaultRecipientsFlag, false) identityEnv := flags.ValueFor[string](cmd, *flags.VaultIdentityEnvFlag, false) identityFile := flags.ValueFor[string](cmd, *flags.VaultIdentityFileFlag, false) - vaultV2.NewAgeVault(vaultName, vaultPath, recipients, identityEnv, identityFile) + vault.NewAgeVault(vaultName, vaultPath, recipients, identityEnv, identityFile) case "keyring": - vaultV2.NewKeyringVault(vaultName) + vault.NewKeyringVault(vaultName) case "external": cfgFile := flags.ValueFor[string](cmd, *flags.VaultFromFileFlag, false) if cfgFile == "" { logger.Log().Fatalf("external vault requires a configuration file to be specified with --config") } - vaultV2.NewExternalVault(vaultPath) + vault.NewExternalVault(vaultPath) default: logger.Log().Fatalf( "unsupported vault type: %s - must be one of 'aes256', 'age', 'unencrypted', 'keyring', or 'external'", @@ -116,7 +114,7 @@ func createVaultFunc(ctx *context.Context, cmd *cobra.Command, args []string) { curWs := ctx.Config.CurrentWorkspace vaultPath = utils.ExpandDirectory( - vaultPath, ctx.Config.Workspaces[curWs], vaultV2.CacheDirectory(vaultName), nil, + vaultPath, ctx.Config.Workspaces[curWs], vault.CacheDirectory(vaultName), nil, ) ctx.Config.Vaults[vaultName] = vaultPath @@ -146,9 +144,7 @@ func registerGetVaultCmd(ctx *context.Context, vaultCmd *cobra.Command) { vaultName = args[0] } - if vaultName == vaultV2.LegacyVaultReservedName { - logger.Log().Fatalf("get is unsupported for the legacy vault") - } else if err := vaultV2.ValidateIdentifier(vaultName); err != nil { + if err := vault.ValidateIdentifier(vaultName); err != nil { logger.Log().Fatalf("invalid vault name '%s': %v", vaultName, err) } @@ -227,7 +223,7 @@ func registerRemoveVaultCmd(ctx *context.Context, vaultCmd *cobra.Command) { func removeVaultFunc(ctx *context.Context, _ *cobra.Command, args []string) { vaultName := args[0] - if vaultName == vaultV2.LegacyVaultReservedName || vaultName == vaultV2.DemoVaultReservedName { + if vaultName == vault.DemoVaultReservedName { logger.Log().Fatalf("remove is unsupported for the current vault") } @@ -279,7 +275,7 @@ func registerSwitchVaultCmd(ctx *context.Context, vaultCmd *cobra.Command) { }, PreRun: func(cmd *cobra.Command, args []string) { vaultName := args[0] - reservedName := vaultName == vaultV2.LegacyVaultReservedName || vaultName == vaultV2.DemoVaultReservedName + reservedName := vaultName == vault.DemoVaultReservedName if reservedName { return } @@ -318,9 +314,9 @@ func registerEditVaultCmd(ctx *context.Context, vaultCmd *cobra.Command) { PreRun: func(cmd *cobra.Command, args []string) { validateVaults(ctx.Config) vaultName := args[0] - if vaultName == vaultV2.LegacyVaultReservedName || vaultName == vaultV2.DemoVaultReservedName { + if vaultName == vault.DemoVaultReservedName { logger.Log().Fatalf("edit is unsupported for the current vault") - } else if err := vaultV2.ValidateIdentifier(vaultName); err != nil { + } else if err := vault.ValidateIdentifier(vaultName); err != nil { logger.Log().Fatalf("invalid vault name '%s': %v", vaultName, err) } @@ -354,7 +350,7 @@ func editVaultFunc(_ *context.Context, cmd *cobra.Command, args []string) { identityEnv := flags.ValueFor[string](cmd, *flags.VaultIdentityEnvFlag, false) identityFile := flags.ValueFor[string](cmd, *flags.VaultIdentityFileFlag, false) - cfgPath := vaultV2.ConfigFilePath(vaultName) + cfgPath := vault.ConfigFilePath(vaultName) existingCfg, err := extvault.LoadConfigJSON(cfgPath) if err != nil { logger.Log().Fatalf("failed to load vault configuration: %v", err) @@ -408,63 +404,8 @@ func editVaultFunc(_ *context.Context, cmd *cobra.Command, args []string) { logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' configuration updated successfully", vaultName)) } -func registerMigrateVaultCmd(ctx *context.Context, vaultCmd *cobra.Command) { - migrateCmd := &cobra.Command{ - Use: "migrate TARGET", - Short: "Migrate the legacy vault to a newer vault.", - Long: "Migrate the legacy vault to a newer vault type. " + - "The target vault must exist and the encryption key must be set for the legacy vault. " + - "Note: This will not remove the legacy vault, but will copy its contents to the target vault.", - Args: cobra.ExactArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return vaultNames(ctx.Config), cobra.ShellCompDirectiveNoFileComp - }, - PreRun: func(cmd *cobra.Command, args []string) { - validateVaults(ctx.Config) - vaultName := args[0] - if vaultName == vaultV2.LegacyVaultReservedName || vaultName == vaultV2.DemoVaultReservedName { - logger.Log().Fatalf("migrate is unsupported for the current vault") - } else if err := vaultV2.ValidateIdentifier(vaultName); err != nil { - logger.Log().Fatalf("invalid vault name '%s': %v", vaultName, err) - } - - userConfig := ctx.Config - if _, found := userConfig.Vaults[vaultName]; !found { - logger.Log().Fatalf("vault %s not found", vaultName) - } - }, - Run: func(cmd *cobra.Command, args []string) { migrateVaultFunc(ctx, cmd, args) }, - } - - vaultCmd.AddCommand(migrateCmd) -} - -func migrateVaultFunc(ctx *context.Context, cmd *cobra.Command, args []string) { - targetVaultName := args[0] - - setAuthEnv(ctx, cmd, nil, true) - legacyVault := vault.NewVault() - _, targetVault, err := vaultV2.VaultFromName(targetVaultName) - if err != nil { - logger.Log().Fatalf("failed to load target vault '%s': %v", targetVaultName, err) - } - defer targetVault.Close() - - s1, err := legacyVault.GetAllSecrets() - if err != nil { - logger.Log().Fatalf("failed to retrieve secrets from legacy vault: %v", err) - } - for name, secret := range s1 { - if err := targetVault.SetSecret(name, vaultV2.NewSecretValue([]byte(secret.PlainTextString()))); err != nil { - logger.Log().Fatalf("failed to migrate secret '%s' to target vault '%s': %v", name, targetVaultName, err) - } - } - - logger.Log().PlainTextSuccess(fmt.Sprintf("Legacy vault migrated to '%s'", targetVaultName)) -} - func vaultNames(cfg *config.Config) []string { - names := []string{vaultV2.LegacyVaultReservedName, vaultV2.DemoVaultReservedName} + names := []string{vault.DemoVaultReservedName} if cfg == nil || cfg.Vaults == nil { return nil } diff --git a/docs/cli/flow_vault.md b/docs/cli/flow_vault.md index 3f38a6c8..f1a73316 100644 --- a/docs/cli/flow_vault.md +++ b/docs/cli/flow_vault.md @@ -22,7 +22,6 @@ Manage sensitive secret stores. * [flow vault edit](flow_vault_edit.md) - Edit the configuration of an existing vault. * [flow vault get](flow_vault_get.md) - Get the details of a vault. * [flow vault list](flow_vault_list.md) - List all available vaults. -* [flow vault migrate](flow_vault_migrate.md) - Migrate the legacy vault to a newer vault. * [flow vault remove](flow_vault_remove.md) - Remove an existing vault. * [flow vault switch](flow_vault_switch.md) - Switch the active vault. diff --git a/docs/cli/flow_vault_migrate.md b/docs/cli/flow_vault_migrate.md deleted file mode 100644 index f08e5557..00000000 --- a/docs/cli/flow_vault_migrate.md +++ /dev/null @@ -1,29 +0,0 @@ -## flow vault migrate - -Migrate the legacy vault to a newer vault. - -### Synopsis - -Migrate the legacy vault to a newer vault type. The target vault must exist and the encryption key must be set for the legacy vault. Note: This will not remove the legacy vault, but will copy its contents to the target vault. - -``` -flow vault migrate TARGET [flags] -``` - -### Options - -``` - -h, --help help for migrate -``` - -### Options inherited from parent commands - -``` - -L, --log-level string Log verbosity level (debug, info, fatal) (default "info") - --sync Sync flow cache and workspaces -``` - -### SEE ALSO - -* [flow vault](flow_vault.md) - Manage sensitive secret stores. - diff --git a/docs/guide/secrets.md b/docs/guide/secrets.md index 095227ef..9a027a1e 100644 --- a/docs/guide/secrets.md +++ b/docs/guide/secrets.md @@ -201,18 +201,6 @@ flow vault create myapp --key-file ~/mykeys/myapp.key flow vault create team --type age --identity-file ~/identities/identity.txt --identity-env MY_IDENTITY ``` -#### Pre-v1 Migration - -If you have a (pre-v1.0) legacy vault, you can migrate it to a v1 vault: - -```shell -flow vault create new-vault --key-env MY_NEW_VAULT_KEY -# Be sure to set the new and old vault keys if needed -flow vault migrate new-vault -``` - -This migrates secrets from the old vault format to the new named vault system. Note that this requires the old vault to be accessible with its key set in the `FLOW_VAULT_KEY` environment variable. - ## Using Secrets in Workflows ### Basic Usage diff --git a/go.mod b/go.mod index 6d62b6b1..2f65d7c2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/flowexec/flow -go 1.24.0 +go 1.24.6 require ( github.com/atotto/clipboard v0.1.4 @@ -24,7 +24,6 @@ require ( github.com/spf13/pflag v1.0.7 go.etcd.io/bbolt v1.4.2 go.uber.org/mock v0.5.2 - golang.org/x/crypto v0.41.0 golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 golang.org/x/sync v0.16.0 gopkg.in/yaml.v3 v3.0.1 @@ -94,6 +93,7 @@ require ( github.com/yuin/goldmark-emoji v1.0.6 // indirect github.com/zalando/go-keyring v0.2.6 // indirect go.uber.org/automaxprocs v1.6.0 // indirect + golang.org/x/crypto v0.41.0 // indirect golang.org/x/net v0.42.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/term v0.34.0 // indirect diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go deleted file mode 100644 index c5236cc8..00000000 --- a/internal/crypto/crypto.go +++ /dev/null @@ -1,113 +0,0 @@ -package crypto - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" - "fmt" - "io" - - "golang.org/x/crypto/scrypt" -) - -// GenerateKey generates a random 32 byte key and returns it as a base64 encoded string. -func GenerateKey() (string, error) { - key := make([]byte, 32) - _, err := rand.Read(key) - if err != nil { - return "", fmt.Errorf("error reading random bytes: %w", err) - } - return EncodeValue(key), nil -} - -// DeriveKey derives a 32 byte key from the provided password and salt and returns -// the key and salt as base64 encoded strings. -// If salt is nil, a random salt will be generated. -func DeriveKey(password, salt []byte) (string, string, error) { - if salt == nil { - salt = make([]byte, 32) - if _, err := rand.Read(salt); err != nil { - return "", "", err - } - } - - key, err := scrypt.Key(password, salt, 1048576, 8, 1, 32) - if err != nil { - return "", "", err - } - - return EncodeValue(key), EncodeValue(salt), nil -} - -// EncodeValue encodes a byte slice as a base64 encoded string. -func EncodeValue(b []byte) string { - return base64.StdEncoding.EncodeToString(b) -} - -// DecodeValue decodes a base64 encoded string into a byte slice. -func DecodeValue(s string) ([]byte, error) { - data, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return nil, err - } - return data, nil -} - -// EncryptValue encrypts a string using AES-256 and returns the encrypted value as a base64 encoded string. -// The encryption key used for encryption must be a base64 encoded string. -func EncryptValue(encryptionKey string, text string) (string, error) { - decodedMasterKey, err := DecodeValue(encryptionKey) - if err != nil { - return "", fmt.Errorf("error decoding master key: %w", err) - } - block, err := aes.NewCipher(decodedMasterKey) - if err != nil { - return "", fmt.Errorf("error creating new cipher: %w", err) - } - - plaintext := []byte(text) - // verify that the plaintext is not too long to fit in an int - if len(plaintext) > 64*1024*1024 { - return "", fmt.Errorf("plaintext too long to encrypt") - } - size := aes.BlockSize + len(plaintext) - ciphertext := make([]byte, size) - iv := ciphertext[:aes.BlockSize] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return "", fmt.Errorf("error reading random bytes: %w", err) - } - - // TODO: replace deprecated NewCFBDecrypter with NewCTR -- this can break vaults - //nolint:staticcheck - cfb := cipher.NewCFBEncrypter(block, iv) - cfb.XORKeyStream(ciphertext[aes.BlockSize:], plaintext) - return string(ciphertext), nil -} - -// DecryptValue decrypts a string using AES-256 and returns the decrypted value as a base64 encoded string. -// The master key used for decryption must be a base64 encoded string. -func DecryptValue(encryptionKey string, text string) (string, error) { - decodedMasterKey, err := DecodeValue(encryptionKey) - if err != nil { - return "", fmt.Errorf("error decoding master key: %w", err) - } - block, err := aes.NewCipher(decodedMasterKey) - if err != nil { - return "", fmt.Errorf("error creating new cipher: %w", err) - } - - ciphertext := []byte(text) - if len(ciphertext) < aes.BlockSize { - return "", fmt.Errorf("ciphertext too short") - } - plainText := make([]byte, len(ciphertext)-aes.BlockSize) - iv := ciphertext[:aes.BlockSize] - ciphertext = ciphertext[aes.BlockSize:] - - // TODO: replace deprecated NewCFBDecrypter with NewCTR -- this can break vaults - //nolint:staticcheck - cfb := cipher.NewCFBDecrypter(block, iv) - cfb.XORKeyStream(plainText, ciphertext) - return string(plainText), nil -} diff --git a/internal/crypto/crypto_test.go b/internal/crypto/crypto_test.go deleted file mode 100644 index c286be7e..00000000 --- a/internal/crypto/crypto_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package crypto_test - -import ( - "testing" - - "github.com/flowexec/flow/internal/crypto" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestCrypto(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Crypto Suite") -} - -var _ = Describe("GenerateKey", func() { - It("generates a key", func() { - key, err := crypto.GenerateKey() - Expect(err).ToNot(HaveOccurred()) - Expect(key).ToNot(BeEmpty()) - - decodedKey, err := crypto.DecodeValue(key) - Expect(err).ToNot(HaveOccurred()) - Expect(decodedKey).ToNot(BeEmpty()) - }) -}) - -var _ = Describe("DeriveKey", func() { - It("derives a key from a password when salt is provided", func() { - salt, err := crypto.GenerateKey() - Expect(err).ToNot(HaveOccurred()) - decodedSalt, err := crypto.DecodeValue(salt) - Expect(err).ToNot(HaveOccurred()) - Expect(decodedSalt).ToNot(BeEmpty()) - - inputPassword := []byte("password") - derivedKey, outSalt, err := crypto.DeriveKey(inputPassword, decodedSalt) - Expect(err).ToNot(HaveOccurred()) - Expect(derivedKey).ToNot(BeEmpty()) - Expect(outSalt).To(Equal(salt)) - - decodedDerivedKey, err := crypto.DecodeValue(derivedKey) - Expect(err).ToNot(HaveOccurred()) - Expect(decodedDerivedKey).ToNot(BeEmpty()) - }) - - It("derives a key from a password when the salt is not provided", func() { - inputPassword := []byte("password") - derivedKey, outSalt, err := crypto.DeriveKey(inputPassword, nil) - Expect(err).ToNot(HaveOccurred()) - Expect(derivedKey).ToNot(BeEmpty()) - Expect(outSalt).ToNot(BeEmpty()) - - decodedDerivedKey, err := crypto.DecodeValue(derivedKey) - Expect(err).ToNot(HaveOccurred()) - Expect(decodedDerivedKey).ToNot(BeEmpty()) - }) -}) - -var _ = Describe("EncryptValue and DecryptValue", func() { - It("encrypts and decrypts a value", func() { - masterKey, _ := crypto.GenerateKey() - plaintext := "test value" - encryptedValue, err := crypto.EncryptValue(masterKey, plaintext) - Expect(err).ToNot(HaveOccurred()) - Expect(encryptedValue).ToNot(BeEmpty()) - Expect(encryptedValue).ToNot(Equal(plaintext)) - - decryptedValue, err := crypto.DecryptValue(masterKey, encryptedValue) - Expect(err).ToNot(HaveOccurred()) - Expect(decryptedValue).ToNot(BeEmpty()) - Expect(decryptedValue).To(Equal(plaintext)) - }) -}) diff --git a/internal/io/secret/output.go b/internal/io/secret/output.go index 5d2b679f..e4d7494a 100644 --- a/internal/io/secret/output.go +++ b/internal/io/secret/output.go @@ -1,48 +1,38 @@ package secret import ( - "encoding/json" "fmt" - "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/io/common" "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/vault" ) -type secretOutput struct { - Secrets map[string]string `json:"secrets" yaml:"secrets"` -} - -func PrintSecrets(ctx *context.Context, secrets map[string]vault.SecretValue, format string, plaintext bool) { - if secrets == nil { - return - } - output := secretOutput{ - Secrets: make(map[string]string, len(secrets)), +func PrintSecrets(ctx *context.Context, vaultName string, vlt vault.Vault, format string, plaintext bool) { + secrets, err := vault.NewSecretList(vaultName, vlt) + if err != nil { + logger.Log().FatalErr(err) } - for key, value := range secrets { - if plaintext { - output.Secrets[key] = value.PlainTextString() - } else { - output.Secrets[key] = value.ObfuscatedString() - } + + if plaintext { + secrets = secrets.AsPlaintext() + } else { + secrets = secrets.AsObfuscatedText() } - // TODO: switch to using the SecretList type or something similar + switch common.NormalizeFormat(format) { case common.YAMLFormat: - str, err := yaml.Marshal(output) + str, err := secrets.YAML() if err != nil { logger.Log().Fatalf("Failed to marshal secrets - %v", err) } - _, _ = fmt.Fprint(ctx.StdOut(), string(str)) + _, _ = fmt.Fprint(ctx.StdOut(), str) case common.JSONFormat: - str, err := json.MarshalIndent(output, "", " ") + str, err := secrets.JSON() if err != nil { logger.Log().Fatalf("Failed to marshal secrets - %v", err) } - _, _ = fmt.Fprint(ctx.StdOut(), string(str)) + _, _ = fmt.Fprint(ctx.StdOut(), str) } } diff --git a/internal/io/secret/v2/output.go b/internal/io/secret/v2/output.go deleted file mode 100644 index e4d7f414..00000000 --- a/internal/io/secret/v2/output.go +++ /dev/null @@ -1,38 +0,0 @@ -package secret - -import ( - "fmt" - - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/io/common" - "github.com/flowexec/flow/internal/logger" - vaultV2 "github.com/flowexec/flow/internal/vault/v2" -) - -func PrintSecrets(ctx *context.Context, vaultName string, vlt vaultV2.Vault, format string, plaintext bool) { - secrets, err := vaultV2.NewSecretList(vaultName, vlt) - if err != nil { - logger.Log().FatalErr(err) - } - - if plaintext { - secrets = secrets.AsPlaintext() - } else { - secrets = secrets.AsObfuscatedText() - } - - switch common.NormalizeFormat(format) { - case common.YAMLFormat: - str, err := secrets.YAML() - if err != nil { - logger.Log().Fatalf("Failed to marshal secrets - %v", err) - } - _, _ = fmt.Fprint(ctx.StdOut(), str) - case common.JSONFormat: - str, err := secrets.JSON() - if err != nil { - logger.Log().Fatalf("Failed to marshal secrets - %v", err) - } - _, _ = fmt.Fprint(ctx.StdOut(), str) - } -} diff --git a/internal/io/secret/v2/views.go b/internal/io/secret/v2/views.go deleted file mode 100644 index a16b187a..00000000 --- a/internal/io/secret/v2/views.go +++ /dev/null @@ -1,194 +0,0 @@ -//nolint:cyclop,funlen -package secret - -import ( - "fmt" - - "github.com/flowexec/tuikit" - "github.com/flowexec/tuikit/themes" - "github.com/flowexec/tuikit/types" - "github.com/flowexec/tuikit/views" - - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/logger" - "github.com/flowexec/flow/internal/vault/v2" -) - -func NewSecretView( - ctx *context.Context, - vlt vault.Vault, - ref vault.SecretRef, - asPlainText bool, -) tuikit.View { - container := ctx.TUIContainer - if ref.Vault() != vlt.ID() { - err := fmt.Errorf( - "failure while initializing the secret view secret: vault mismatch -expected %s, got %s", - vlt.ID(), - ref.Vault(), - ) - container.HandleError(err) - return nil - } - - s, err := vlt.GetSecret(ref.Key()) - if err != nil { - container.HandleError(fmt.Errorf("failure while initializing the secret view secret: %w", err)) - return nil - } - - secret, err := vault.NewSecret(vlt.ID(), ref.Key(), s) - if err != nil { - container.HandleError(fmt.Errorf("failure while initializing the secret view secret: %w", err)) - return nil - } - if asPlainText { - secret = secret.AsPlaintext() - } else { - secret = secret.AsObfuscatedText() - } - - loadSecretList := func() { - view := NewSecretListView(ctx, vlt, asPlainText) - if err := ctx.SetView(view); err != nil { - logger.Log().FatalErr(err) - } - } - - var secretKeyCallbacks = []types.KeyCallback{ - { - Key: "r", Label: "rename", - Callback: func() error { - form, err := views.NewFormView( - container.RenderState(), - &views.FormField{ - Key: "value", - Type: views.PromptTypeText, - Title: "Enter the new secret name", - }) - if err != nil { - container.HandleError(fmt.Errorf("encountered error creating the form: %w", err)) - return nil - } - if err := ctx.SetView(form); err != nil { - container.HandleError(fmt.Errorf("unable to set view: %w", err)) - return nil - } - newName := form.FindByKey("value").Value() - if err := vlt.SetSecret(newName, secret); err != nil { - container.HandleError(fmt.Errorf("unable to set secret with new name: %w", err)) - return nil - } - if err := vlt.DeleteSecret(ref.Key()); err != nil { - container.HandleError(fmt.Errorf("unable to delete old secret: %w", err)) - return nil - } - loadSecretList() - container.SetNotice("secret renamed", themes.OutputLevelInfo) - return nil - }, - }, - { - Key: "e", Label: "edit", - Callback: func() error { - form, err := views.NewFormView( - container.RenderState(), - &views.FormField{ - Key: "value", - Type: views.PromptTypeMasked, - Title: "Enter the new secret value", - }) - if err != nil { - container.HandleError(fmt.Errorf("encountered error creating the form: %w", err)) - return nil - } - if err := ctx.SetView(form); err != nil { - container.HandleError(fmt.Errorf("unable to set view: %w", err)) - return nil - } - newValue := form.FindByKey("value").Value() - secretValue := vault.NewSecretValue([]byte(newValue)) - if err := vlt.SetSecret(ref.Key(), secretValue); err != nil { - container.HandleError(fmt.Errorf("unable to edit secret: %w", err)) - return nil - } - loadSecretList() - container.SetNotice("secret value updated", themes.OutputLevelInfo) - return nil - }, - }, - { - Key: "x", Label: "delete", - Callback: func() error { - if err := vlt.DeleteSecret(ref.Key()); err != nil { - container.HandleError(fmt.Errorf("unable to delete secret: %w", err)) - return nil - } - loadSecretList() - container.SetNotice("secret deleted", themes.OutputLevelInfo) - return nil - }, - }, - } - - return views.NewEntityView(container.RenderState(), secret, types.EntityFormatDocument, secretKeyCallbacks...) -} - -func NewSecretListView( - ctx *context.Context, - vlt vault.Vault, - asPlainText bool, -) tuikit.View { - container := ctx.TUIContainer - - keys, err := vlt.ListSecrets() - if err != nil { - container.HandleError(fmt.Errorf("failed to list secrets: %w", err)) - return nil - } - secrets := make(vault.SecretList, 0, len(keys)) - for _, key := range keys { - s, err := vlt.GetSecret(key) - if err != nil { - container.HandleError(fmt.Errorf("failed to get secret %s: %w", key, err)) - continue - } - secret, err := vault.NewSecret(vlt.ID(), key, s) - if err != nil { - container.HandleError(fmt.Errorf("failed to create secret object for %s: %w", key, err)) - continue - } - if asPlainText { - secret = secret.AsPlaintext() - } else { - secret = secret.AsObfuscatedText() - } - secrets = append(secrets, secret) - } - - if len(secrets.Items()) == 0 { - container.HandleError(fmt.Errorf("no secrets found")) - } - - selectFunc := func(filterVal string) error { - var secret vault.Secret - var found bool - for _, s := range secrets { - if s == nil { - continue - } - if string(s.Ref()) == filterVal { - secret = s - found = true - break - } - } - if !found || secret == nil { - return fmt.Errorf("secret not found") - } - - return container.SetView(NewSecretView(ctx, vlt, secret.Ref(), asPlainText)) - } - - return views.NewCollectionView(container.RenderState(), secrets, types.CollectionFormatList, selectFunc) -} diff --git a/internal/io/secret/views.go b/internal/io/secret/views.go index 331bc791..3509efe7 100644 --- a/internal/io/secret/views.go +++ b/internal/io/secret/views.go @@ -1,3 +1,4 @@ +//nolint:cyclop,funlen package secret import ( @@ -10,16 +11,50 @@ import ( "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/logger" - "github.com/flowexec/flow/internal/vault" + vault2 "github.com/flowexec/flow/internal/vault" ) func NewSecretView( ctx *context.Context, - secret vault.Secret, + vlt vault2.Vault, + ref vault2.SecretRef, asPlainText bool, ) tuikit.View { container := ctx.TUIContainer - v := vault.NewVault() + if ref.Vault() != vlt.ID() { + err := fmt.Errorf( + "failure while initializing the secret view secret: vault mismatch -expected %s, got %s", + vlt.ID(), + ref.Vault(), + ) + container.HandleError(err) + return nil + } + + s, err := vlt.GetSecret(ref.Key()) + if err != nil { + container.HandleError(fmt.Errorf("failure while initializing the secret view secret: %w", err)) + return nil + } + + secret, err := vault2.NewSecret(vlt.ID(), ref.Key(), s) + if err != nil { + container.HandleError(fmt.Errorf("failure while initializing the secret view secret: %w", err)) + return nil + } + if asPlainText { + secret = secret.AsPlaintext() + } else { + secret = secret.AsObfuscatedText() + } + + loadSecretList := func() { + view := NewSecretListView(ctx, vlt, asPlainText) + if err := ctx.SetView(view); err != nil { + logger.Log().FatalErr(err) + } + } + var secretKeyCallbacks = []types.KeyCallback{ { Key: "r", Label: "rename", @@ -40,11 +75,15 @@ func NewSecretView( return nil } newName := form.FindByKey("value").Value() - if err := v.RenameSecret(secret.Reference, newName); err != nil { - container.HandleError(fmt.Errorf("unable to rename secret: %w", err)) + if err := vlt.SetSecret(newName, secret); err != nil { + container.HandleError(fmt.Errorf("unable to set secret with new name: %w", err)) + return nil + } + if err := vlt.DeleteSecret(ref.Key()); err != nil { + container.HandleError(fmt.Errorf("unable to delete old secret: %w", err)) return nil } - LoadSecretListView(ctx, asPlainText) + loadSecretList() container.SetNotice("secret renamed", themes.OutputLevelInfo) return nil }, @@ -68,12 +107,12 @@ func NewSecretView( return nil } newValue := form.FindByKey("value").Value() - secretValue := vault.SecretValue(newValue) - if err := v.SetSecret(secret.Reference, secretValue); err != nil { + secretValue := vault2.NewSecretValue([]byte(newValue)) + if err := vlt.SetSecret(ref.Key(), secretValue); err != nil { container.HandleError(fmt.Errorf("unable to edit secret: %w", err)) return nil } - LoadSecretListView(ctx, asPlainText) + loadSecretList() container.SetNotice("secret value updated", themes.OutputLevelInfo) return nil }, @@ -81,73 +120,75 @@ func NewSecretView( { Key: "x", Label: "delete", Callback: func() error { - if err := v.DeleteSecret(secret.Reference); err != nil { + if err := vlt.DeleteSecret(ref.Key()); err != nil { container.HandleError(fmt.Errorf("unable to delete secret: %w", err)) return nil } - LoadSecretListView(ctx, asPlainText) + loadSecretList() container.SetNotice("secret deleted", themes.OutputLevelInfo) return nil }, }, } - return views.NewEntityView(container.RenderState(), &secret, types.EntityFormatDocument, secretKeyCallbacks...) + return views.NewEntityView(container.RenderState(), secret, types.EntityFormatDocument, secretKeyCallbacks...) } func NewSecretListView( ctx *context.Context, - secrets vault.SecretList, + vlt vault2.Vault, asPlainText bool, ) tuikit.View { container := ctx.TUIContainer + + keys, err := vlt.ListSecrets() + if err != nil { + container.HandleError(fmt.Errorf("failed to list secrets: %w", err)) + return nil + } + secrets := make(vault2.SecretList, 0, len(keys)) + for _, key := range keys { + s, err := vlt.GetSecret(key) + if err != nil { + container.HandleError(fmt.Errorf("failed to get secret %s: %w", key, err)) + continue + } + secret, err := vault2.NewSecret(vlt.ID(), key, s) + if err != nil { + container.HandleError(fmt.Errorf("failed to create secret object for %s: %w", key, err)) + continue + } + if asPlainText { + secret = secret.AsPlaintext() + } else { + secret = secret.AsObfuscatedText() + } + secrets = append(secrets, secret) + } + if len(secrets.Items()) == 0 { container.HandleError(fmt.Errorf("no secrets found")) } selectFunc := func(filterVal string) error { - var secret vault.Secret + var secret vault2.Secret var found bool for _, s := range secrets { - if s.Reference == filterVal { + if s == nil { + continue + } + if string(s.Ref()) == filterVal { secret = s found = true break } } - if !found { + if !found || secret == nil { return fmt.Errorf("secret not found") } - return container.SetView(NewSecretView(ctx, secret, asPlainText)) + return container.SetView(NewSecretView(ctx, vlt, secret.Ref(), asPlainText)) } return views.NewCollectionView(container.RenderState(), secrets, types.CollectionFormatList, selectFunc) } - -func LoadSecretListView( - ctx *context.Context, - asPlainText bool, -) { - v := vault.NewVault() - secrets, err := v.GetAllSecrets() - if err != nil { - logger.Log().FatalErr(err) - } - var secretList vault.SecretList - for name, secret := range secrets { - if asPlainText { - secretList = append(secretList, vault.Secret{Reference: name, Secret: secret.PlainTextString()}) - } else { - secretList = append(secretList, vault.Secret{Reference: name, Secret: secret.ObfuscatedString()}) - } - } - view := NewSecretListView( - ctx, - secretList, - asPlainText, - ) - if err := ctx.SetView(view); err != nil { - logger.Log().FatalErr(err) - } -} diff --git a/internal/io/vault/view.go b/internal/io/vault/view.go index ab6c58d7..191521c2 100644 --- a/internal/io/vault/view.go +++ b/internal/io/vault/view.go @@ -12,7 +12,7 @@ import ( "golang.org/x/exp/maps" "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/vault/v2" + "github.com/flowexec/flow/internal/vault" ) type vaultEntity struct { diff --git a/internal/utils/env/params.go b/internal/utils/env/params.go index 5deccf51..b6c08d6a 100644 --- a/internal/utils/env/params.go +++ b/internal/utils/env/params.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/flowexec/flow/internal/vault" - vaultV2 "github.com/flowexec/flow/internal/vault/v2" "github.com/flowexec/flow/types/executable" ) @@ -42,34 +41,22 @@ func resolveSecretValue( currentVault string, secretRef string, ) (string, error) { - //nolint:nestif - if currentVault == "" { - if err := vault.ValidateReference(secretRef); err != nil { - return "", err - } - v := vault.NewVault() - secret, err := v.GetSecret(secretRef) - if err != nil { - return "", err - } - return secret.PlainTextString(), nil - } else { - rVault, key, err := vaultV2.RefToParts(vaultV2.SecretRef(secretRef)) - if err != nil { - return "", err - } - if rVault == "" { - rVault = currentVault - } - _, v, err := vaultV2.VaultFromName(rVault) - if err != nil { - return "", err - } - defer v.Close() - secret, err := v.GetSecret(key) - if err != nil { - return "", err - } - return secret.PlainTextString(), nil + + rVault, key, err := vault.RefToParts(vault.SecretRef(secretRef)) + if err != nil { + return "", err + } + if rVault == "" { + rVault = currentVault + } + _, v, err := vault.VaultFromName(rVault) + if err != nil { + return "", err + } + defer v.Close() + secret, err := v.GetSecret(key) + if err != nil { + return "", err } + return secret.PlainTextString(), nil } diff --git a/internal/vault/v2/demo.go b/internal/vault/demo.go similarity index 100% rename from internal/vault/v2/demo.go rename to internal/vault/demo.go diff --git a/internal/vault/secret.go b/internal/vault/secret.go index 76247246..28c5575f 100644 --- a/internal/vault/secret.go +++ b/internal/vault/secret.go @@ -2,76 +2,241 @@ package vault import ( "encoding/json" + "errors" "fmt" + "regexp" + "strings" "github.com/flowexec/tuikit/types" + "github.com/flowexec/vault" "gopkg.in/yaml.v3" ) -type SecretValue string +type SecretRef string -func (s SecretValue) ObfuscatedString() string { - if s.Empty() { - return "" +func (r SecretRef) Key() string { + parts := strings.Split(string(r), "/") + if len(parts) < 2 { + return string(r) } - return "********" + return parts[1] } -func (s SecretValue) String() string { - return s.ObfuscatedString() +func (r SecretRef) Vault() string { + parts := strings.Split(string(r), "/") + if len(parts) < 2 { + return "" + } + return parts[0] } -func (s SecretValue) PlainTextString() string { - return string(s) +type Secret interface { + vault.Secret + types.Entity + + Ref() SecretRef + AsPlaintext() Secret + AsObfuscatedText() Secret } -func (s SecretValue) Empty() bool { - return string(s) == "" +type SecretValue = vault.SecretValue + +type secret struct { + vault string + key string + plaintext bool + value vault.Secret } -type Secret struct { - Reference string `json:"reference" yaml:"reference"` - Secret string `json:"value" yaml:"value"` +// enrichedSecret is used for JSON/YAML marshaling to control how the value is serialized +type enrichedSecret struct { + Vault string `json:"vault" yaml:"vault"` + Key string `json:"key" yaml:"key"` + Value string `json:"value" yaml:"value"` } -func NewSecret(reference string, secret string) *Secret { - if err := ValidateReference(reference); err != nil { - return nil +func NewSecret(vaultName, key string, value vault.Secret) (Secret, error) { + if err := ValidateIdentifier(vaultName); err != nil { + return nil, err + } + if key == "" { + return nil, errors.New("key cannot be empty") + } else if vaultName == "" { + return nil, errors.New("vault name cannot be empty") } - return &Secret{Reference: reference, Secret: secret} + + return &secret{ + vault: vaultName, + key: key, + value: value, + }, nil } -type SecretList []Secret +func NewSecretValue(value []byte) *SecretValue { + return vault.NewSecretValue(value) +} -type enrichedSecretList struct { - Secrets SecretList `json:"secrets" yaml:"secrets"` +func (s *secret) Ref() SecretRef { + return SecretRef(fmt.Sprintf("%s/%s", s.vault, s.key)) +} + +func (s *secret) AsPlaintext() Secret { + s.plaintext = true + return s } -func (c *Secret) YAML() (string, error) { - yamlBytes, err := yaml.Marshal(c) +func (s *secret) AsObfuscatedText() Secret { + s.plaintext = false + return s +} + +func (s *secret) YAML() (string, error) { + yamlBytes, err := yaml.Marshal(toEnrichedSecret(s)) if err != nil { return "", fmt.Errorf("failed to marshal secret - %w", err) } return string(yamlBytes), nil } -func (c *Secret) JSON() (string, error) { - jsonBytes, err := json.MarshalIndent(c, "", " ") +func (s *secret) JSON() (string, error) { + jsonBytes, err := json.MarshalIndent(toEnrichedSecret(s), "", " ") if err != nil { return "", fmt.Errorf("failed to marshal secret - %w", err) } return string(jsonBytes), nil } -func (c *Secret) Markdown() string { +func (s *secret) Markdown() string { var mkdwn string - mkdwn = fmt.Sprintf("# [Secret] %s\n", c.Reference) - mkdwn += fmt.Sprintf("**Value**\n```\n%s\n```", c.Secret) + + mkdwn = fmt.Sprintf("# [Secret] %s\n", s.Ref()) + + valueStr := s.value.String() + if s.plaintext { + valueStr = s.value.PlainTextString() + } + + mkdwn += fmt.Sprintf("**Value**\n```\n%s\n```", valueStr) return mkdwn } +func (s *secret) String() string { + return s.value.String() +} + +func (s *secret) PlainTextString() string { + return s.value.PlainTextString() +} + +func (s *secret) Bytes() []byte { + return s.value.Bytes() +} + +func (s *secret) Zero() { + s.value.Zero() +} + +func RefToParts(ref SecretRef) (vaultName, key string, err error) { + parts := strings.Split(string(ref), "/") + if len(parts) == 1 { + return "", parts[0], nil + } else if len(parts) != 2 { + return "", "", fmt.Errorf("invalid secret reference format: %s", ref) + } + vaultName = parts[0] + key = parts[1] + if key == "" || vaultName == "" { + return "", "", fmt.Errorf("vault name and key cannot be empty: %s", ref) + } + return vaultName, key, nil +} + +func toEnrichedSecret(s Secret) enrichedSecret { + valueStr := s.String() // Default to obfuscated + if s.AsPlaintext() != nil { + valueStr = s.PlainTextString() + } + + return enrichedSecret{ + Vault: s.Ref().Vault(), + Key: s.Ref().Key(), + Value: valueStr, + } +} + +// toEnrichedSecretWithMode allows explicit control over plaintext vs obfuscated +func toEnrichedSecretWithMode(s Secret, plaintext bool) enrichedSecret { + valueStr := s.String() + if plaintext { + valueStr = s.PlainTextString() + } + + return enrichedSecret{ + Vault: s.Ref().Vault(), + Key: s.Ref().Key(), + Value: valueStr, + } +} + +type SecretList []Secret + +func NewSecretList(vaultName string, v Vault) (SecretList, error) { + secrets, err := v.ListSecrets() + if err != nil { + return nil, err + } + + result := make(SecretList, 0, len(secrets)) + for _, key := range secrets { + s, _ := v.GetSecret(key) + if s == nil { + continue + } + scrt, err := NewSecret(vaultName, key, s) + if err != nil { + return nil, err + } + result = append(result, scrt) + } + + return result, nil +} + +type enrichedSecretList struct { + Secrets []enrichedSecret `json:"secrets" yaml:"secrets"` +} + +func (l SecretList) AsPlaintext() SecretList { + result := make(SecretList, 0, len(l)) + for i, s := range l { + if s == nil { + continue + } + result[i] = s.AsPlaintext() + } + return result +} + +func (l SecretList) AsObfuscatedText() SecretList { + result := make(SecretList, len(l)) + for i, s := range l { + if s == nil { + continue + } + result[i] = s.AsObfuscatedText() + } + return result +} + func (l SecretList) YAML() (string, error) { - enriched := enrichedSecretList{Secrets: l} + scrts := make([]enrichedSecret, 0, len(l)) + for _, s := range l { + if s == nil { + continue + } + scrts = append(scrts, toEnrichedSecret(s)) + } + enriched := enrichedSecretList{Secrets: scrts} yamlBytes, err := yaml.Marshal(enriched) if err != nil { return "", fmt.Errorf("failed to marshal secret list - %w", err) @@ -80,7 +245,48 @@ func (l SecretList) YAML() (string, error) { } func (l SecretList) JSON() (string, error) { - enriched := enrichedSecretList{Secrets: l} + scrts := make([]enrichedSecret, 0, len(l)) + for _, s := range l { + if s == nil { + continue + } + scrts = append(scrts, toEnrichedSecret(s)) + } + enriched := enrichedSecretList{Secrets: scrts} + jsonBytes, err := json.MarshalIndent(enriched, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal secret list - %w", err) + } + return string(jsonBytes), nil +} + +// YAMLWithMode allows explicit control over plaintext vs obfuscated serialization +func (l SecretList) YAMLWithMode(plaintext bool) (string, error) { + scrts := make([]enrichedSecret, 0, len(l)) + for _, s := range l { + if s == nil { + continue + } + scrts = append(scrts, toEnrichedSecretWithMode(s, plaintext)) + } + enriched := enrichedSecretList{Secrets: scrts} + yamlBytes, err := yaml.Marshal(enriched) + if err != nil { + return "", fmt.Errorf("failed to marshal secret list - %w", err) + } + return string(yamlBytes), nil +} + +// JSONWithMode allows explicit control over plaintext vs obfuscated serialization +func (l SecretList) JSONWithMode(plaintext bool) (string, error) { + scrts := make([]enrichedSecret, 0, len(l)) + for _, s := range l { + if s == nil { + continue + } + scrts = append(scrts, toEnrichedSecretWithMode(s, plaintext)) + } + enriched := enrichedSecretList{Secrets: scrts} jsonBytes, err := json.MarshalIndent(enriched, "", " ") if err != nil { return "", fmt.Errorf("failed to marshal secret list - %w", err) @@ -88,10 +294,10 @@ func (l SecretList) JSON() (string, error) { return string(jsonBytes), nil } -func (l SecretList) FindByName(name string) *Secret { - for _, secret := range l { - if secret.Reference == name { - return &secret +func (l SecretList) FindByName(name string) Secret { + for _, scrt := range l { + if scrt.Ref().Key() == name { + return scrt } } return nil @@ -99,10 +305,10 @@ func (l SecretList) FindByName(name string) *Secret { func (l SecretList) Items() []*types.EntityInfo { items := make([]*types.EntityInfo, 0) - for _, secret := range l { + for _, s := range l { item := types.EntityInfo{ - Header: secret.Reference, - ID: secret.Reference, + Header: s.Ref().Key(), + ID: string(s.Ref()), } items = append(items, &item) } @@ -116,3 +322,17 @@ func (l SecretList) Singular() string { func (l SecretList) Plural() string { return "secrets" } + +func ValidateIdentifier(reference string) error { + if reference == "" { + return errors.New("reference cannot be empty") + } + re := regexp.MustCompile(`^[a-zA-Z0-9-_]+$`) + if !re.MatchString(reference) { + return fmt.Errorf( + "reference (%s) must only contain alphanumeric characters, dashes and/or underscores", + reference, + ) + } + return nil +} diff --git a/internal/vault/v2/secret.go b/internal/vault/v2/secret.go deleted file mode 100644 index 28c5575f..00000000 --- a/internal/vault/v2/secret.go +++ /dev/null @@ -1,338 +0,0 @@ -package vault - -import ( - "encoding/json" - "errors" - "fmt" - "regexp" - "strings" - - "github.com/flowexec/tuikit/types" - "github.com/flowexec/vault" - "gopkg.in/yaml.v3" -) - -type SecretRef string - -func (r SecretRef) Key() string { - parts := strings.Split(string(r), "/") - if len(parts) < 2 { - return string(r) - } - return parts[1] -} - -func (r SecretRef) Vault() string { - parts := strings.Split(string(r), "/") - if len(parts) < 2 { - return "" - } - return parts[0] -} - -type Secret interface { - vault.Secret - types.Entity - - Ref() SecretRef - AsPlaintext() Secret - AsObfuscatedText() Secret -} - -type SecretValue = vault.SecretValue - -type secret struct { - vault string - key string - plaintext bool - value vault.Secret -} - -// enrichedSecret is used for JSON/YAML marshaling to control how the value is serialized -type enrichedSecret struct { - Vault string `json:"vault" yaml:"vault"` - Key string `json:"key" yaml:"key"` - Value string `json:"value" yaml:"value"` -} - -func NewSecret(vaultName, key string, value vault.Secret) (Secret, error) { - if err := ValidateIdentifier(vaultName); err != nil { - return nil, err - } - if key == "" { - return nil, errors.New("key cannot be empty") - } else if vaultName == "" { - return nil, errors.New("vault name cannot be empty") - } - - return &secret{ - vault: vaultName, - key: key, - value: value, - }, nil -} - -func NewSecretValue(value []byte) *SecretValue { - return vault.NewSecretValue(value) -} - -func (s *secret) Ref() SecretRef { - return SecretRef(fmt.Sprintf("%s/%s", s.vault, s.key)) -} - -func (s *secret) AsPlaintext() Secret { - s.plaintext = true - return s -} - -func (s *secret) AsObfuscatedText() Secret { - s.plaintext = false - return s -} - -func (s *secret) YAML() (string, error) { - yamlBytes, err := yaml.Marshal(toEnrichedSecret(s)) - if err != nil { - return "", fmt.Errorf("failed to marshal secret - %w", err) - } - return string(yamlBytes), nil -} - -func (s *secret) JSON() (string, error) { - jsonBytes, err := json.MarshalIndent(toEnrichedSecret(s), "", " ") - if err != nil { - return "", fmt.Errorf("failed to marshal secret - %w", err) - } - return string(jsonBytes), nil -} - -func (s *secret) Markdown() string { - var mkdwn string - - mkdwn = fmt.Sprintf("# [Secret] %s\n", s.Ref()) - - valueStr := s.value.String() - if s.plaintext { - valueStr = s.value.PlainTextString() - } - - mkdwn += fmt.Sprintf("**Value**\n```\n%s\n```", valueStr) - return mkdwn -} - -func (s *secret) String() string { - return s.value.String() -} - -func (s *secret) PlainTextString() string { - return s.value.PlainTextString() -} - -func (s *secret) Bytes() []byte { - return s.value.Bytes() -} - -func (s *secret) Zero() { - s.value.Zero() -} - -func RefToParts(ref SecretRef) (vaultName, key string, err error) { - parts := strings.Split(string(ref), "/") - if len(parts) == 1 { - return "", parts[0], nil - } else if len(parts) != 2 { - return "", "", fmt.Errorf("invalid secret reference format: %s", ref) - } - vaultName = parts[0] - key = parts[1] - if key == "" || vaultName == "" { - return "", "", fmt.Errorf("vault name and key cannot be empty: %s", ref) - } - return vaultName, key, nil -} - -func toEnrichedSecret(s Secret) enrichedSecret { - valueStr := s.String() // Default to obfuscated - if s.AsPlaintext() != nil { - valueStr = s.PlainTextString() - } - - return enrichedSecret{ - Vault: s.Ref().Vault(), - Key: s.Ref().Key(), - Value: valueStr, - } -} - -// toEnrichedSecretWithMode allows explicit control over plaintext vs obfuscated -func toEnrichedSecretWithMode(s Secret, plaintext bool) enrichedSecret { - valueStr := s.String() - if plaintext { - valueStr = s.PlainTextString() - } - - return enrichedSecret{ - Vault: s.Ref().Vault(), - Key: s.Ref().Key(), - Value: valueStr, - } -} - -type SecretList []Secret - -func NewSecretList(vaultName string, v Vault) (SecretList, error) { - secrets, err := v.ListSecrets() - if err != nil { - return nil, err - } - - result := make(SecretList, 0, len(secrets)) - for _, key := range secrets { - s, _ := v.GetSecret(key) - if s == nil { - continue - } - scrt, err := NewSecret(vaultName, key, s) - if err != nil { - return nil, err - } - result = append(result, scrt) - } - - return result, nil -} - -type enrichedSecretList struct { - Secrets []enrichedSecret `json:"secrets" yaml:"secrets"` -} - -func (l SecretList) AsPlaintext() SecretList { - result := make(SecretList, 0, len(l)) - for i, s := range l { - if s == nil { - continue - } - result[i] = s.AsPlaintext() - } - return result -} - -func (l SecretList) AsObfuscatedText() SecretList { - result := make(SecretList, len(l)) - for i, s := range l { - if s == nil { - continue - } - result[i] = s.AsObfuscatedText() - } - return result -} - -func (l SecretList) YAML() (string, error) { - scrts := make([]enrichedSecret, 0, len(l)) - for _, s := range l { - if s == nil { - continue - } - scrts = append(scrts, toEnrichedSecret(s)) - } - enriched := enrichedSecretList{Secrets: scrts} - yamlBytes, err := yaml.Marshal(enriched) - if err != nil { - return "", fmt.Errorf("failed to marshal secret list - %w", err) - } - return string(yamlBytes), nil -} - -func (l SecretList) JSON() (string, error) { - scrts := make([]enrichedSecret, 0, len(l)) - for _, s := range l { - if s == nil { - continue - } - scrts = append(scrts, toEnrichedSecret(s)) - } - enriched := enrichedSecretList{Secrets: scrts} - jsonBytes, err := json.MarshalIndent(enriched, "", " ") - if err != nil { - return "", fmt.Errorf("failed to marshal secret list - %w", err) - } - return string(jsonBytes), nil -} - -// YAMLWithMode allows explicit control over plaintext vs obfuscated serialization -func (l SecretList) YAMLWithMode(plaintext bool) (string, error) { - scrts := make([]enrichedSecret, 0, len(l)) - for _, s := range l { - if s == nil { - continue - } - scrts = append(scrts, toEnrichedSecretWithMode(s, plaintext)) - } - enriched := enrichedSecretList{Secrets: scrts} - yamlBytes, err := yaml.Marshal(enriched) - if err != nil { - return "", fmt.Errorf("failed to marshal secret list - %w", err) - } - return string(yamlBytes), nil -} - -// JSONWithMode allows explicit control over plaintext vs obfuscated serialization -func (l SecretList) JSONWithMode(plaintext bool) (string, error) { - scrts := make([]enrichedSecret, 0, len(l)) - for _, s := range l { - if s == nil { - continue - } - scrts = append(scrts, toEnrichedSecretWithMode(s, plaintext)) - } - enriched := enrichedSecretList{Secrets: scrts} - jsonBytes, err := json.MarshalIndent(enriched, "", " ") - if err != nil { - return "", fmt.Errorf("failed to marshal secret list - %w", err) - } - return string(jsonBytes), nil -} - -func (l SecretList) FindByName(name string) Secret { - for _, scrt := range l { - if scrt.Ref().Key() == name { - return scrt - } - } - return nil -} - -func (l SecretList) Items() []*types.EntityInfo { - items := make([]*types.EntityInfo, 0) - for _, s := range l { - item := types.EntityInfo{ - Header: s.Ref().Key(), - ID: string(s.Ref()), - } - items = append(items, &item) - } - return items -} - -func (l SecretList) Singular() string { - return "secret" -} - -func (l SecretList) Plural() string { - return "secrets" -} - -func ValidateIdentifier(reference string) error { - if reference == "" { - return errors.New("reference cannot be empty") - } - re := regexp.MustCompile(`^[a-zA-Z0-9-_]+$`) - if !re.MatchString(reference) { - return fmt.Errorf( - "reference (%s) must only contain alphanumeric characters, dashes and/or underscores", - reference, - ) - } - return nil -} diff --git a/internal/vault/v2/vault.go b/internal/vault/v2/vault.go deleted file mode 100644 index 88937547..00000000 --- a/internal/vault/v2/vault.go +++ /dev/null @@ -1,277 +0,0 @@ -package vault - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/flowexec/vault" - - "github.com/flowexec/flow/internal/filesystem" - "github.com/flowexec/flow/internal/logger" - "github.com/flowexec/flow/internal/utils" -) - -const ( - DefaultVaultKeyEnv = "FLOW_VAULT_KEY" - DefaultVaultIdentityEnv = "FLOW_VAULT_IDENTITY" - LegacyVaultReservedName = "legacy" - - v2CacheDataDir = "vaults" - keyringService = "io.flowexec.flow" -) - -type Vault = vault.Provider -type VaultConfig = vault.Config - -func NewAES256Vault(name, storagePath, keyEnv, keyFile, logLevel string) { - if keyEnv == "" { - logger.Log().Debugf("no AES key provided, using default environment variable %s", DefaultVaultKeyEnv) - keyEnv = DefaultVaultKeyEnv - } else { - logger.Log().Debugf("using AES key from environment variable %s", keyEnv) - } - - key := os.Getenv(keyEnv) - if key == "" { - key = generateAESKey(keyEnv, logLevel) - // this key needs to be set when initializing the vault - if err := os.Setenv(keyEnv, key); err != nil { - logger.Log().FatalErr(fmt.Errorf("unable to set environment variable %s: %w", keyEnv, err)) - } - } else { - logger.Log().Debugf("using existing AES key from environment variable %s", keyEnv) - } - - storagePath = utils.ExpandPath(storagePath, CacheDirectory(""), nil) - if storagePath == "" { - logger.Log().Fatalf("unable to expand storage path: %s", storagePath) - } - - opts := []vault.Option{ - vault.WithAESPath(storagePath), - vault.WithProvider(vault.ProviderTypeAES256), - vault.WithAESKeyFromEnv(keyEnv), - } - - if keyFile != "" { - keyFile = utils.ExpandPath(keyFile, CacheDirectory(""), nil) - if keyFile == "" { - logger.Log().Fatalf("unable to expand key file path: %s", keyFile) - } - opts = append(opts, vault.WithAESKeyFromFile(keyFile)) - if err := writeKeyToFile(key, keyFile); err != nil { - logger.Log().Warnx("unable to write key to file", "err", err) - } - } - - v, cfg, err := vault.New(name, opts...) - if err != nil { - logger.Log().FatalErr(err) - } - - cfgPath := ConfigFilePath(v.ID()) - if err = vault.SaveConfigJSON(*cfg, cfgPath); err != nil { - logger.Log().FatalErr(fmt.Errorf("unable to save vault config: %w", err)) - } - - if logLevel != "fatal" { - logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' with AES256 encryption created successfully", v.ID())) - } -} - -func generateAESKey(keyEnv, logLevel string) string { - key, err := vault.GenerateEncryptionKey() - if err != nil { - logger.Log().FatalErr(err) - } - - if logLevel != "fatal" { - logger.Log().PlainTextSuccess(fmt.Sprintf("Your vault encryption key is: %s", key)) - newKeyMsg := fmt.Sprintf( - "You will need this key to modify your vault data. Store it somewhere safe!\n"+ - "Set this value to the %s environment variable to access the vault in the future.\n", - keyEnv, - ) - logger.Log().PlainTextInfo(newKeyMsg) - } else { - // just print the key without additional info - logger.Log().Print(key) - } - return key -} - -func NewUnencryptedVault(name, storagePath string) { - storagePath = utils.ExpandPath(storagePath, CacheDirectory(""), nil) - if storagePath == "" { - logger.Log().Fatalf("unable to expand storage path: %s", storagePath) - } - - opts := []vault.Option{vault.WithUnencryptedPath(storagePath), vault.WithProvider(vault.ProviderTypeUnencrypted)} - - v, cfg, err := vault.New(name, opts...) - if err != nil { - logger.Log().FatalErr(err) - } - - cfgPath := ConfigFilePath(v.ID()) - if err = vault.SaveConfigJSON(*cfg, cfgPath); err != nil { - logger.Log().FatalErr(fmt.Errorf("unable to save vault config: %w", err)) - } - - logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' without encryption created successfully", v.ID())) -} - -func NewAgeVault(name, storagePath, recipients, identityKey, identityFile string) { - storagePath = utils.ExpandPath(storagePath, CacheDirectory(""), nil) - if storagePath == "" { - logger.Log().Fatalf("unable to expand storage path: %s", storagePath) - } - - opts := []vault.Option{vault.WithAgePath(storagePath), vault.WithProvider(vault.ProviderTypeAge)} - if recipients != "" { - opts = append(opts, vault.WithAgeRecipients(strings.Split(recipients, ",")...)) - } - if identityKey != "" { - opts = append(opts, vault.WithAgeIdentityFromEnv(identityKey)) - } - if identityFile != "" { - identityFile = utils.ExpandPath(identityFile, CacheDirectory(""), nil) - opts = append(opts, vault.WithAgeIdentityFromFile(identityFile)) - } - - if identityKey == "" && identityFile == "" { - logger.Log().Debugf("no Age identity provided, using default environment variable %s", DefaultVaultIdentityEnv) - opts = append(opts, vault.WithAgeIdentityFromEnv(DefaultVaultIdentityEnv)) - } - - v, cfg, err := vault.New(name, opts...) - if err != nil { - logger.Log().FatalErr(err) - } - - cfgPath := ConfigFilePath(v.ID()) - if err = vault.SaveConfigJSON(*cfg, cfgPath); err != nil { - logger.Log().FatalErr(fmt.Errorf("unable to save vault config: %w", err)) - } - - logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' with Age encryption created successfully", v.ID())) -} - -func NewKeyringVault(name string) { - opts := []vault.Option{ - vault.WithKeyringService(fmt.Sprintf("%s.%s", keyringService, name)), - vault.WithProvider(vault.ProviderTypeKeyring)} - v, cfg, err := vault.New(name, opts...) - if err != nil { - logger.Log().FatalErr(err) - } - - cfgPath := ConfigFilePath(v.ID()) - if err = vault.SaveConfigJSON(*cfg, cfgPath); err != nil { - logger.Log().FatalErr(fmt.Errorf("unable to save vault config: %w", err)) - } - - logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' with Keyring encryption created successfully", v.ID())) -} - -func NewExternalVault(providerConfigFile string) { - if providerConfigFile == "" { - logger.Log().Fatalf("provider config file path cannot be empty") - } - - providerConfigFile = utils.ExpandPath(providerConfigFile, CacheDirectory(""), nil) - if providerConfigFile == "" { - logger.Log().Fatalf("unable to expand provider config file path: %s", providerConfigFile) - } - - cfg, err := vault.LoadConfigJSON(providerConfigFile) - if err != nil { - logger.Log().FatalErr(fmt.Errorf("failed to load vault config: %w", err)) - } - - v, _, err := vault.New(cfg.ID, vault.WithExternalConfig(cfg.External)) - if err != nil { - logger.Log().FatalErr(err) - } - - cfgPath := ConfigFilePath(v.ID()) - if err = vault.SaveConfigJSON(cfg, cfgPath); err != nil { - logger.Log().FatalErr(fmt.Errorf("unable to save vault config: %w", err)) - } - - logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' with external provider registered successfully", v.ID())) -} - -func VaultFromName(name string) (*VaultConfig, Vault, error) { - if name == "" { - return nil, nil, fmt.Errorf("vault name cannot be empty") - } else if strings.ToLower(name) == DemoVaultReservedName { - return newDemoVaultConfig(), newDemoVault(), nil - } - - cfgPath := ConfigFilePath(name) - cfg, err := vault.LoadConfigJSON(cfgPath) - if err != nil { - return nil, nil, fmt.Errorf("failed to load vault config: %w", err) - } - - switch cfg.Type { - case vault.ProviderTypeAge: - provider, err := vault.NewAgeVault(&cfg) - return &cfg, provider, err - case vault.ProviderTypeAES256: - provider, err := vault.NewAES256Vault(&cfg) - return &cfg, provider, err - case vault.ProviderTypeUnencrypted: - provider, err := vault.NewUnencryptedVault(&cfg) - return &cfg, provider, err - case vault.ProviderTypeKeyring: - provider, err := vault.NewKeyringVault(&cfg) - return &cfg, provider, err - case vault.ProviderTypeExternal: - // todo: rename this func in the vault pkg - provider, err := vault.NewExternalVaultProvider(&cfg) - return &cfg, provider, err - default: - return nil, nil, fmt.Errorf("unsupported vault type: %s", cfg.Type) - } -} - -func CacheDirectory(subPath string) string { - return filepath.Join(filesystem.CachedDataDirPath(), v2CacheDataDir, subPath) -} - -func ConfigFilePath(vaultName string) string { - return filepath.Join( - filesystem.CachedDataDirPath(), - v2CacheDataDir, - fmt.Sprintf("configs/%s.json", vaultName), - ) -} - -func writeKeyToFile(key, filePath string) error { - if key == "" { - return nil - } - if filePath == "" { - return fmt.Errorf("no file path provided to write key") - } - - if _, err := os.Stat(filePath); err == nil { - logger.Log().Debugf("key file already exists at %s, skipping write", filePath) - return nil - } - - if err := os.MkdirAll(filepath.Dir(filePath), 0750); err != nil { - return fmt.Errorf("unable to create directory for key file: %w", err) - } - - if err := os.WriteFile(filePath, []byte(key), 0600); err != nil { - return fmt.Errorf("unable to write key to file: %w", err) - } - logger.Log().Infof("Key written to file: %s", filePath) - - return nil -} diff --git a/internal/vault/vault.go b/internal/vault/vault.go index e0cebb7b..1d0775d3 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -1,267 +1,276 @@ package vault import ( - "crypto/sha512" "fmt" - stdio "io" "os" "path/filepath" - "regexp" - "time" + "strings" - "github.com/pkg/errors" - "gopkg.in/yaml.v3" + "github.com/flowexec/vault" - "github.com/flowexec/flow/internal/crypto" "github.com/flowexec/flow/internal/filesystem" "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/internal/utils" ) const ( - EncryptionKeyEnvVar = "FLOW_VAULT_KEY" + DefaultVaultKeyEnv = "FLOW_VAULT_KEY" + DefaultVaultIdentityEnv = "FLOW_VAULT_IDENTITY" - cacheDirName = "vault" + v2CacheDataDir = "vaults" + keyringService = "io.flowexec.flow" ) -// Deprecated: Use the github.com/flowexec/vault package instead. -// This vault will be removed in a future release. -type Vault struct { - cachedEncryptionKey string - cachedData *data -} - -// Represents the data stored in the vault data file. -type data struct { - LastUpdated string `yaml:"lastUpdated"` - Secrets map[string]SecretValue `yaml:"secrets"` -} +type Vault = vault.Provider +type VaultConfig = vault.Config -func RegisterEncryptionKey(key string) error { - if err := filesystem.EnsureCachedDataDir(); err != nil { - return err +func NewAES256Vault(name, storagePath, keyEnv, keyFile, logLevel string) { + if keyEnv == "" { + logger.Log().Debugf("no AES key provided, using default environment variable %s", DefaultVaultKeyEnv) + keyEnv = DefaultVaultKeyEnv + } else { + logger.Log().Debugf("using AES key from environment variable %s", keyEnv) } - fullPath := dataFilePath(key) - if _, err := os.Stat(fullPath); !os.IsNotExist(err) { - return errors.New("encryption key already registered") + key := os.Getenv(keyEnv) + if key == "" { + key = generateAESKey(keyEnv, logLevel) + // this key needs to be set when initializing the vault + if err := os.Setenv(keyEnv, key); err != nil { + logger.Log().FatalErr(fmt.Errorf("unable to set environment variable %s: %w", keyEnv, err)) + } + } else { + logger.Log().Debugf("using existing AES key from environment variable %s", keyEnv) } - dir, _ := filepath.Split(fullPath) - if err := os.MkdirAll(dir, 0750); err != nil { - return errors.Wrap(err, "unable to create vault data directory") - } - if _, err := os.Create(filepath.Clean(fullPath)); err != nil { - return errors.Wrap(err, "unable to create vault data file") + storagePath = utils.ExpandPath(storagePath, CacheDirectory(""), nil) + if storagePath == "" { + logger.Log().Fatalf("unable to expand storage path: %s", storagePath) } - return nil -} + opts := []vault.Option{ + vault.WithAESPath(storagePath), + vault.WithProvider(vault.ProviderTypeAES256), + vault.WithAESKeyFromEnv(keyEnv), + } -func NewVault() *Vault { - return &Vault{} -} + if keyFile != "" { + keyFile = utils.ExpandPath(keyFile, CacheDirectory(""), nil) + if keyFile == "" { + logger.Log().Fatalf("unable to expand key file path: %s", keyFile) + } + opts = append(opts, vault.WithAESKeyFromFile(keyFile)) + if err := writeKeyToFile(key, keyFile); err != nil { + logger.Log().Warnx("unable to write key to file", "err", err) + } + } -func (v *Vault) GetSecret(reference string) (SecretValue, error) { - logger.Log().Debugf("getting secret with reference %s from vault", reference) - d, err := v.loadData() + v, cfg, err := vault.New(name, opts...) if err != nil { - return "", err - } else if d == nil { - return "", errors.New("no secrets found in vault") + logger.Log().FatalErr(err) } - secret, found := d.Secrets[reference] - if !found { - return "", fmt.Errorf("secret with reference %s not found", reference) + cfgPath := ConfigFilePath(v.ID()) + if err = vault.SaveConfigJSON(*cfg, cfgPath); err != nil { + logger.Log().FatalErr(fmt.Errorf("unable to save vault config: %w", err)) } - return secret, nil -} -func (v *Vault) GetAllSecrets() (map[string]SecretValue, error) { - logger.Log().Debugf("getting all secrets from vault") - d, err := v.loadData() - if err != nil { - return nil, err - } else if d == nil { - return nil, errors.New("no secrets found in vault") + if logLevel != "fatal" { + logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' with AES256 encryption created successfully", v.ID())) } - return d.Secrets, nil } -func (v *Vault) SetSecret(reference string, secret SecretValue) error { - logger.Log().Debugf("setting secret with reference %s in vault", reference) - if err := ValidateReference(reference); err != nil { - return err +func generateAESKey(keyEnv, logLevel string) string { + key, err := vault.GenerateEncryptionKey() + if err != nil { + logger.Log().FatalErr(err) } - d, err := v.loadData() - if err != nil { - return err + if logLevel != "fatal" { + logger.Log().PlainTextSuccess(fmt.Sprintf("Your vault encryption key is: %s", key)) + newKeyMsg := fmt.Sprintf( + "You will need this key to modify your vault data. Store it somewhere safe!\n"+ + "Set this value to the %s environment variable to access the vault in the future.\n", + keyEnv, + ) + logger.Log().PlainTextInfo(newKeyMsg) + } else { + // just print the key without additional info + logger.Log().Print(key) } + return key +} - if d.Secrets == nil { - d.Secrets = make(map[string]SecretValue) +func NewUnencryptedVault(name, storagePath string) { + storagePath = utils.ExpandPath(storagePath, CacheDirectory(""), nil) + if storagePath == "" { + logger.Log().Fatalf("unable to expand storage path: %s", storagePath) } - d.Secrets[reference] = secret - return v.saveData(d) -} + opts := []vault.Option{vault.WithUnencryptedPath(storagePath), vault.WithProvider(vault.ProviderTypeUnencrypted)} -func (v *Vault) DeleteSecret(reference string) error { - logger.Log().Debugf("deleting secret with reference %s from vault", reference) - d, err := v.loadData() + v, cfg, err := vault.New(name, opts...) if err != nil { - return err + logger.Log().FatalErr(err) } - delete(d.Secrets, reference) + cfgPath := ConfigFilePath(v.ID()) + if err = vault.SaveConfigJSON(*cfg, cfgPath); err != nil { + logger.Log().FatalErr(fmt.Errorf("unable to save vault config: %w", err)) + } - return v.saveData(d) + logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' without encryption created successfully", v.ID())) } -func (v *Vault) RenameSecret(oldRef string, newRef string) error { - logger.Log().Debugf("renaming secret with reference %s in vault", oldRef) - - d, err := v.loadData() - if err != nil { - return err +func NewAgeVault(name, storagePath, recipients, identityKey, identityFile string) { + storagePath = utils.ExpandPath(storagePath, CacheDirectory(""), nil) + if storagePath == "" { + logger.Log().Fatalf("unable to expand storage path: %s", storagePath) } - secret, exists := d.Secrets[oldRef] - if !exists { - return errors.Errorf("secret with reference %s does not exist in vault", oldRef) + opts := []vault.Option{vault.WithAgePath(storagePath), vault.WithProvider(vault.ProviderTypeAge)} + if recipients != "" { + opts = append(opts, vault.WithAgeRecipients(strings.Split(recipients, ",")...)) } - - _, exists = d.Secrets[newRef] - if exists { - return errors.Errorf("secret with reference %s already exists in vault", newRef) + if identityKey != "" { + opts = append(opts, vault.WithAgeIdentityFromEnv(identityKey)) + } + if identityFile != "" { + identityFile = utils.ExpandPath(identityFile, CacheDirectory(""), nil) + opts = append(opts, vault.WithAgeIdentityFromFile(identityFile)) } - if err := ValidateReference(newRef); err != nil { - return err + if identityKey == "" && identityFile == "" { + logger.Log().Debugf("no Age identity provided, using default environment variable %s", DefaultVaultIdentityEnv) + opts = append(opts, vault.WithAgeIdentityFromEnv(DefaultVaultIdentityEnv)) } - d.Secrets[newRef] = secret + v, cfg, err := vault.New(name, opts...) + if err != nil { + logger.Log().FatalErr(err) + } - delete(d.Secrets, oldRef) + cfgPath := ConfigFilePath(v.ID()) + if err = vault.SaveConfigJSON(*cfg, cfgPath); err != nil { + logger.Log().FatalErr(fmt.Errorf("unable to save vault config: %w", err)) + } - return v.saveData(d) + logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' with Age encryption created successfully", v.ID())) } -func (v *Vault) retrieveEncryptionKey() (string, error) { - if v.cachedEncryptionKey != "" { - return v.cachedEncryptionKey, nil +func NewKeyringVault(name string) { + opts := []vault.Option{ + vault.WithKeyringService(fmt.Sprintf("%s.%s", keyringService, name)), + vault.WithProvider(vault.ProviderTypeKeyring)} + v, cfg, err := vault.New(name, opts...) + if err != nil { + logger.Log().FatalErr(err) } - key, found := os.LookupEnv(EncryptionKeyEnvVar) - if !found { - return "", errors.New("encryption key not set") - } - if err := validateEncryptionKey(key); err != nil { - return "", err + cfgPath := ConfigFilePath(v.ID()) + if err = vault.SaveConfigJSON(*cfg, cfgPath); err != nil { + logger.Log().FatalErr(fmt.Errorf("unable to save vault config: %w", err)) } - return key, nil + + logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' with Keyring encryption created successfully", v.ID())) } -func (v *Vault) loadData() (*data, error) { - if v.cachedData != nil { - return v.cachedData, nil +func NewExternalVault(providerConfigFile string) { + if providerConfigFile == "" { + logger.Log().Fatalf("provider config file path cannot be empty") } - key, err := v.retrieveEncryptionKey() - if err != nil { - return nil, err + providerConfigFile = utils.ExpandPath(providerConfigFile, CacheDirectory(""), nil) + if providerConfigFile == "" { + logger.Log().Fatalf("unable to expand provider config file path: %s", providerConfigFile) } - fullPath := dataFilePath(key) - file, err := os.Open(filepath.Clean(fullPath)) + cfg, err := vault.LoadConfigJSON(providerConfigFile) if err != nil { - return nil, errors.Wrap(err, "unable to open vault data file") + logger.Log().FatalErr(fmt.Errorf("failed to load vault config: %w", err)) } - defer file.Close() - encryptedDataStr, err := stdio.ReadAll(file) + v, _, err := vault.New(cfg.ID, vault.WithExternalConfig(cfg.External)) if err != nil { - return nil, errors.Wrap(err, "unable to read vault data file") - } - - if len(encryptedDataStr) == 0 { - return &data{}, nil + logger.Log().FatalErr(err) } - dataStr, err := crypto.DecryptValue(key, string(encryptedDataStr)) - if err != nil { - return nil, errors.Wrap(err, "unable to decrypt vault data") + cfgPath := ConfigFilePath(v.ID()) + if err = vault.SaveConfigJSON(cfg, cfgPath); err != nil { + logger.Log().FatalErr(fmt.Errorf("unable to save vault config: %w", err)) } - var d data - if err := yaml.Unmarshal([]byte(dataStr), &d); err != nil { - return nil, errors.Wrap(err, "unable to unmarshal vault data") - } - return &d, nil + logger.Log().PlainTextSuccess(fmt.Sprintf("Vault '%s' with external provider registered successfully", v.ID())) } -func (v *Vault) saveData(d *data) error { - if d == nil { - return nil +func VaultFromName(name string) (*VaultConfig, Vault, error) { + if name == "" { + return nil, nil, fmt.Errorf("vault name cannot be empty") + } else if strings.ToLower(name) == DemoVaultReservedName { + return newDemoVaultConfig(), newDemoVault(), nil } - key, err := v.retrieveEncryptionKey() + cfgPath := ConfigFilePath(name) + cfg, err := vault.LoadConfigJSON(cfgPath) if err != nil { - return err + return nil, nil, fmt.Errorf("failed to load vault config: %w", err) + } + + switch cfg.Type { + case vault.ProviderTypeAge: + provider, err := vault.NewAgeVault(&cfg) + return &cfg, provider, err + case vault.ProviderTypeAES256: + provider, err := vault.NewAES256Vault(&cfg) + return &cfg, provider, err + case vault.ProviderTypeUnencrypted: + provider, err := vault.NewUnencryptedVault(&cfg) + return &cfg, provider, err + case vault.ProviderTypeKeyring: + provider, err := vault.NewKeyringVault(&cfg) + return &cfg, provider, err + case vault.ProviderTypeExternal: + // todo: rename this func in the vault pkg + provider, err := vault.NewExternalVaultProvider(&cfg) + return &cfg, provider, err + default: + return nil, nil, fmt.Errorf("unsupported vault type: %s", cfg.Type) } +} - d.LastUpdated = time.Now().Format(time.RFC3339) - dataStr, err := yaml.Marshal(d) - if err != nil { - return errors.Wrap(err, "unable to marshal vault data") +func CacheDirectory(subPath string) string { + return filepath.Join(filesystem.CachedDataDirPath(), v2CacheDataDir, subPath) +} + +func ConfigFilePath(vaultName string) string { + return filepath.Join( + filesystem.CachedDataDirPath(), + v2CacheDataDir, + fmt.Sprintf("configs/%s.json", vaultName), + ) +} + +func writeKeyToFile(key, filePath string) error { + if key == "" { + return nil } - encryptedDataStr, err := crypto.EncryptValue(key, string(dataStr)) - if err != nil { - return errors.Wrap(err, "unable to encrypt vault data") + if filePath == "" { + return fmt.Errorf("no file path provided to write key") } - fullPath := dataFilePath(key) - file, err := os.OpenFile(filepath.Clean(fullPath), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return errors.Wrap(err, "unable to open vault data file") + if _, err := os.Stat(filePath); err == nil { + logger.Log().Debugf("key file already exists at %s, skipping write", filePath) + return nil } - defer file.Close() - if _, err := file.WriteString(encryptedDataStr); err != nil { - return errors.Wrap(err, "unable to write to vault data file") + if err := os.MkdirAll(filepath.Dir(filePath), 0750); err != nil { + return fmt.Errorf("unable to create directory for key file: %w", err) } - return nil -} -func ValidateReference(reference string) error { - if reference == "" { - return errors.New("reference cannot be empty") + if err := os.WriteFile(filePath, []byte(key), 0600); err != nil { + return fmt.Errorf("unable to write key to file: %w", err) } - re := regexp.MustCompile(`^[a-zA-Z0-9-_]+$`) - if !re.MatchString(reference) { - return fmt.Errorf( - "reference (%s) must only contain alphanumeric characters, dashes and/or underscores", - reference, - ) - } - return nil -} + logger.Log().Infof("Key written to file: %s", filePath) -func validateEncryptionKey(key string) error { - expectedDataPath := dataFilePath(key) - if _, err := os.Stat(expectedDataPath); os.IsNotExist(err) { - return errors.New("encryption key not recognized") - } return nil } - -func dataFilePath(encryptionKey string) string { - hasher := sha512.New() - _, err := hasher.Write([]byte(encryptionKey)) - if err != nil { - panic("unable to hash encryption key") - } - storageKey := crypto.EncodeValue(hasher.Sum(nil)) - return filepath.Join(filesystem.CachedDataDirPath(), cacheDirName, storageKey, "data") -}