Öffentliche Dateiansicht: Raw-Dateien, Tree, Releases und Issues sind ohne Login verfügbar.
cmd/root.go Raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
package cmd

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"envault/vault"

	"github.com/spf13/cobra"
)

var (
	flagProject   string
	flagVaultPath string
)

var rootCmd = &cobra.Command{
	Use:   "ev",
	Short: "Local encrypted secret manager",
	Long: `ev stores secrets encrypted outside your project directory,
keeping them away from AI coding agents, git repos, and logs.`,
}

// SetVersion injects the build-time version into the root command.
func SetVersion(v string) {
	rootCmd.Version = v
}

func Execute() {
	if err := rootCmd.Execute(); err != nil {
		os.Exit(1)
	}
}

func init() {
	rootCmd.PersistentFlags().StringVarP(&flagProject, "project", "p", "", "project name (overrides .envault file)")
	rootCmd.PersistentFlags().StringVar(&flagVaultPath, "vault", "", "vault file path (default: ~/.envault/vault.json)")
}

// resolveProject returns the active project name.
// Priority: --project flag > .envault (searched upward) > current directory name
func resolveProject() string {
	if flagProject != "" {
		return flagProject
	}
	cwd, err := os.Getwd()
	if err != nil {
		return "default"
	}
	if name, err := findProjectFile(cwd); err == nil && name != "" {
		return name
	}
	// Warn if a .envault file exists locally but couldn't be parsed
	if _, err := os.Stat(filepath.Join(cwd, ".envault")); err == nil {
		fmt.Fprintln(os.Stderr, "Warning: .envault file found but could not be parsed — falling back to directory name")
	}
	return filepath.Base(cwd)
}

// findProjectFile walks up the directory tree from start looking for a .envault file.
// Stops at a git root (.git directory) or filesystem root to avoid picking up
// unrelated .envault files from parent repositories or home directories.
func findProjectFile(start string) (string, error) {
	dir := start
	for {
		name, err := parseProjectFile(filepath.Join(dir, ".envault"))
		if err == nil {
			return name, nil
		}

		// Stop at git repository root — don't leak into parent repos
		if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
			break
		}

		parent := filepath.Dir(dir)
		if parent == dir {
			break // filesystem root reached
		}
		dir = parent
	}
	return "", fmt.Errorf(".envault not found")
}

// parseProjectFile reads the project name from a single .envault file.
func parseProjectFile(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		parts := strings.SplitN(line, "=", 2)
		if len(parts) == 2 && strings.TrimSpace(parts[0]) == "project" {
			return strings.TrimSpace(parts[1]), nil
		}
		return line, nil
	}
	return "", fmt.Errorf("empty .envault file")
}

// resolveVaultPath returns the active vault path.
func resolveVaultPath() (string, error) {
	if flagVaultPath != "" {
		return flagVaultPath, nil
	}
	return vault.DefaultVaultPath()
}

// trySession returns cached vars from an active session, or nil if none exists.
// Commands that only read secrets should call this first before prompting for a password.
func trySession() map[string]string {
	vars, _ := vault.LoadSession(resolveProject())
	return vars
}

// openVaultKeychain opens the vault using the password stored in macOS Keychain.
// Falls back to a password prompt if no Keychain entry exists.
func openVaultKeychain() (*vault.Vault, string, string, error) {
	path, err := resolveVaultPath()
	if err != nil {
		return nil, "", "", err
	}
	password, err := vault.KeychainGet()
	if err != nil {
		fmt.Fprintln(os.Stderr, "No password in Keychain — prompting.")
		return openVault()
	}
	v, err := vault.Open(path, password)
	if err != nil {
		return nil, "", "", err
	}
	return v, path, password, nil
}

// openVault prompts for the master password and opens (or creates) the vault.
// If the vault is new, it asks to set a password with confirmation.
func openVault() (*vault.Vault, string, string, error) {
	path, err := resolveVaultPath()
	if err != nil {
		return nil, "", "", err
	}

	var password string
	if !vault.Exists(path) {
		fmt.Fprintln(os.Stderr, "No vault found – creating a new one.")
		p1, err := vault.PromptPassword("Set master password: ")
		if err != nil {
			return nil, "", "", err
		}
		if p1 == "" {
			return nil, "", "", errors.New("master password must not be empty")
		}
		p2, err := vault.PromptPassword("Confirm master password: ")
		if err != nil {
			return nil, "", "", err
		}
		if p1 != p2 {
			return nil, "", "", errors.New("passwords do not match")
		}
		password = p1
	} else {
		password, err = vault.PromptPassword("Master password: ")
		if err != nil {
			return nil, "", "", err
		}
	}

	v, err := vault.Open(path, password)
	if err != nil {
		return nil, "", "", err
	}
	return v, path, password, nil
}