Compare commits
	
		
			7 Commits
		
	
	
		
			3d90388b5b
			...
			4b59d6fb82
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4b59d6fb82 | |||
| 5ca657c104 | |||
| bbaf1cbd97 | |||
| f838c8cb98 | |||
| 43767c725f | |||
| b26794e21a | |||
| 7dc14da4af | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					.DS_Store
 | 
				
			||||||
 | 
					**/.DS_Store
 | 
				
			||||||
 | 
					/secret
 | 
				
			||||||
							
								
								
									
										85
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										85
									
								
								Makefile
									
									
									
									
									
								
							@ -1,93 +1,22 @@
 | 
				
			|||||||
# Makefile for Secret Manager - Simple Go CLI Tool
 | 
					default: check
 | 
				
			||||||
 | 
					 | 
				
			||||||
# Configuration
 | 
					 | 
				
			||||||
BINARY_NAME = secret
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
default: build
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Simple build (no code signing needed)
 | 
					# Simple build (no code signing needed)
 | 
				
			||||||
build: clean
 | 
					./secret:
 | 
				
			||||||
	@echo "Building secret manager..."
 | 
						go build -v -o $@ cmd/secret/main.go
 | 
				
			||||||
	go build -o $(BINARY_NAME) cmd/secret/main.go
 | 
					 | 
				
			||||||
	@echo "Build complete: ./$(BINARY_NAME)"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Build with verbose output
 | 
					 | 
				
			||||||
build-verbose: clean
 | 
					 | 
				
			||||||
	@echo "Building with verbose output..."
 | 
					 | 
				
			||||||
	go build -v -o $(BINARY_NAME) cmd/secret/main.go
 | 
					 | 
				
			||||||
	@echo "Build complete: ./$(BINARY_NAME)"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Vet the code
 | 
					 | 
				
			||||||
vet:
 | 
					vet:
 | 
				
			||||||
	@echo "Running go vet..."
 | 
					 | 
				
			||||||
	go vet ./...
 | 
						go vet ./...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test with linting and vetting
 | 
					test:
 | 
				
			||||||
test: vet lint
 | 
					 | 
				
			||||||
	@echo "Running go tests..."
 | 
					 | 
				
			||||||
	go test -v ./...
 | 
						go test -v ./...
 | 
				
			||||||
 | 
						bash test_secret_manager.sh
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Run comprehensive test script
 | 
					 | 
				
			||||||
test-comprehensive: build
 | 
					 | 
				
			||||||
	@echo "Running comprehensive test script..."
 | 
					 | 
				
			||||||
	@chmod +x test_secret_manager.sh
 | 
					 | 
				
			||||||
	@./test_secret_manager.sh
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Run all tests (unit tests + comprehensive tests)
 | 
					 | 
				
			||||||
test-all: test test-comprehensive
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Lint the code
 | 
					 | 
				
			||||||
lint:
 | 
					lint:
 | 
				
			||||||
	@echo "Running linter..."
 | 
					 | 
				
			||||||
	golangci-lint run --timeout 5m
 | 
						golangci-lint run --timeout 5m
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Check all code quality (build + vet + lint + unit tests)
 | 
					# Check all code quality (build + vet + lint + unit tests)
 | 
				
			||||||
check: build vet lint test
 | 
					check: ./secret vet lint test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Clean build artifacts
 | 
					# Clean build artifacts
 | 
				
			||||||
clean:
 | 
					clean:
 | 
				
			||||||
	rm -f ./$(BINARY_NAME)
 | 
						rm -f ./secret
 | 
				
			||||||
 | 
					 | 
				
			||||||
# Install to /usr/local/bin
 | 
					 | 
				
			||||||
install: build
 | 
					 | 
				
			||||||
	@echo "Installing to /usr/local/bin..."
 | 
					 | 
				
			||||||
	sudo cp $(BINARY_NAME) /usr/local/bin/
 | 
					 | 
				
			||||||
	@echo "Installed to /usr/local/bin/$(BINARY_NAME)"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Uninstall from /usr/local/bin
 | 
					 | 
				
			||||||
uninstall:
 | 
					 | 
				
			||||||
	@echo "Removing from /usr/local/bin..."
 | 
					 | 
				
			||||||
	sudo rm -f /usr/local/bin/$(BINARY_NAME)
 | 
					 | 
				
			||||||
	@echo "Uninstalled $(BINARY_NAME)"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Test keychain functionality
 | 
					 | 
				
			||||||
test-keychain:
 | 
					 | 
				
			||||||
	@echo "Testing keychain functionality..."
 | 
					 | 
				
			||||||
	@./$(BINARY_NAME) --help > /dev/null 2>&1 && echo "Binary runs successfully" || echo "Binary failed to run"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# Help target
 | 
					 | 
				
			||||||
help:
 | 
					 | 
				
			||||||
	@echo "Secret Manager - Simple Go CLI Tool"
 | 
					 | 
				
			||||||
	@echo "===================================="
 | 
					 | 
				
			||||||
	@echo ""
 | 
					 | 
				
			||||||
	@echo "Available targets:"
 | 
					 | 
				
			||||||
	@echo "  build              - Build the secret manager (default)"
 | 
					 | 
				
			||||||
	@echo "  build-verbose      - Build with verbose output"
 | 
					 | 
				
			||||||
	@echo "  vet                - Run go vet"
 | 
					 | 
				
			||||||
	@echo "  lint               - Run linter only"
 | 
					 | 
				
			||||||
	@echo "  test               - Run unit tests with vet and lint"
 | 
					 | 
				
			||||||
	@echo "  test-comprehensive - Run comprehensive test script"
 | 
					 | 
				
			||||||
	@echo "  test-all           - Run both unit tests and comprehensive tests"
 | 
					 | 
				
			||||||
	@echo "  check              - Run all code quality checks"
 | 
					 | 
				
			||||||
	@echo "  clean              - Remove build artifacts"
 | 
					 | 
				
			||||||
	@echo "  install            - Install to /usr/local/bin"
 | 
					 | 
				
			||||||
	@echo "  uninstall          - Remove from /usr/local/bin"
 | 
					 | 
				
			||||||
	@echo "  test-keychain      - Test basic functionality"
 | 
					 | 
				
			||||||
	@echo "  help               - Show this help"
 | 
					 | 
				
			||||||
	@echo ""
 | 
					 | 
				
			||||||
	@echo "Usage:"
 | 
					 | 
				
			||||||
	@echo "  make build && ./secret --help"
 | 
					 | 
				
			||||||
	@echo "  make test-all      # Run all tests"
 | 
					 | 
				
			||||||
	@echo "  make check         # Run all quality checks"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.PHONY: default build build-verbose vet test test-comprehensive test-all lint check clean install uninstall test-keychain help
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -95,7 +95,12 @@ func getStdinScanner() *bufio.Scanner {
 | 
				
			|||||||
// readLineFromStdin reads a single line from stdin with a prompt
 | 
					// readLineFromStdin reads a single line from stdin with a prompt
 | 
				
			||||||
// Uses a shared scanner to avoid buffering issues between multiple calls
 | 
					// Uses a shared scanner to avoid buffering issues between multiple calls
 | 
				
			||||||
func readLineFromStdin(prompt string) (string, error) {
 | 
					func readLineFromStdin(prompt string) (string, error) {
 | 
				
			||||||
	fmt.Print(prompt)
 | 
						// Check if stderr is a terminal - if not, we can't prompt interactively
 | 
				
			||||||
 | 
						if !term.IsTerminal(int(syscall.Stderr)) {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("cannot prompt for input: stderr is not a terminal (running in non-interactive mode)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
 | 
				
			||||||
	scanner := getStdinScanner()
 | 
						scanner := getStdinScanner()
 | 
				
			||||||
	if !scanner.Scan() {
 | 
						if !scanner.Scan() {
 | 
				
			||||||
		if err := scanner.Err(); err != nil {
 | 
							if err := scanner.Err(); err != nil {
 | 
				
			||||||
@ -108,6 +113,7 @@ func readLineFromStdin(prompt string) (string, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// CLIEntry is the entry point for the secret CLI application
 | 
					// CLIEntry is the entry point for the secret CLI application
 | 
				
			||||||
func CLIEntry() {
 | 
					func CLIEntry() {
 | 
				
			||||||
 | 
						Debug("CLIEntry starting - debug output is working")
 | 
				
			||||||
	cmd := newRootCmd()
 | 
						cmd := newRootCmd()
 | 
				
			||||||
	if err := cmd.Execute(); err != nil {
 | 
						if err := cmd.Execute(); err != nil {
 | 
				
			||||||
		os.Exit(1)
 | 
							os.Exit(1)
 | 
				
			||||||
@ -115,6 +121,7 @@ func CLIEntry() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newRootCmd() *cobra.Command {
 | 
					func newRootCmd() *cobra.Command {
 | 
				
			||||||
 | 
						Debug("newRootCmd starting")
 | 
				
			||||||
	cmd := &cobra.Command{
 | 
						cmd := &cobra.Command{
 | 
				
			||||||
		Use:   "secret",
 | 
							Use:   "secret",
 | 
				
			||||||
		Short: "A simple secrets manager",
 | 
							Short: "A simple secrets manager",
 | 
				
			||||||
@ -124,6 +131,7 @@ func newRootCmd() *cobra.Command {
 | 
				
			|||||||
		SilenceErrors: false,
 | 
							SilenceErrors: false,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Adding subcommands to root command")
 | 
				
			||||||
	// Add subcommands
 | 
						// Add subcommands
 | 
				
			||||||
	cmd.AddCommand(newInitCmd())
 | 
						cmd.AddCommand(newInitCmd())
 | 
				
			||||||
	cmd.AddCommand(newGenerateCmd())
 | 
						cmd.AddCommand(newGenerateCmd())
 | 
				
			||||||
@ -137,6 +145,7 @@ func newRootCmd() *cobra.Command {
 | 
				
			|||||||
	cmd.AddCommand(newEncryptCmd())
 | 
						cmd.AddCommand(newEncryptCmd())
 | 
				
			||||||
	cmd.AddCommand(newDecryptCmd())
 | 
						cmd.AddCommand(newDecryptCmd())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("newRootCmd completed")
 | 
				
			||||||
	return cmd
 | 
						return cmd
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -280,9 +289,12 @@ func newAddCmd() *cobra.Command {
 | 
				
			|||||||
		Long:  `Add a secret to the current vault. The secret value is read from stdin.`,
 | 
							Long:  `Add a secret to the current vault. The secret value is read from stdin.`,
 | 
				
			||||||
		Args:  cobra.ExactArgs(1),
 | 
							Args:  cobra.ExactArgs(1),
 | 
				
			||||||
		RunE: func(cmd *cobra.Command, args []string) error {
 | 
							RunE: func(cmd *cobra.Command, args []string) error {
 | 
				
			||||||
 | 
								Debug("Add command RunE starting", "secret_name", args[0])
 | 
				
			||||||
			force, _ := cmd.Flags().GetBool("force")
 | 
								force, _ := cmd.Flags().GetBool("force")
 | 
				
			||||||
 | 
								Debug("Got force flag", "force", force)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			cli := NewCLIInstance()
 | 
								cli := NewCLIInstance()
 | 
				
			||||||
 | 
								Debug("Created CLI instance, calling AddSecret")
 | 
				
			||||||
			return cli.AddSecret(args[0], force)
 | 
								return cli.AddSecret(args[0], force)
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -528,7 +540,7 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Create default vault
 | 
						// Create default vault
 | 
				
			||||||
	Debug("Creating default vault")
 | 
						Debug("Creating default vault")
 | 
				
			||||||
	_, err = CreateVault(cli.fs, cli.stateDir, "default")
 | 
						vault, err := CreateVault(cli.fs, cli.stateDir, "default")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to create default vault", "error", err)
 | 
							Debug("Failed to create default vault", "error", err)
 | 
				
			||||||
		return fmt.Errorf("failed to create default vault: %w", err)
 | 
							return fmt.Errorf("failed to create default vault: %w", err)
 | 
				
			||||||
@ -550,6 +562,9 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
 | 
				
			|||||||
		return fmt.Errorf("failed to write long-term public key: %w", err)
 | 
							return fmt.Errorf("failed to write long-term public key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Unlock the vault with the derived long-term key
 | 
				
			||||||
 | 
						vault.Unlock(ltIdentity)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Prompt for passphrase for unlock key
 | 
						// Prompt for passphrase for unlock key
 | 
				
			||||||
	var passphraseStr string
 | 
						var passphraseStr string
 | 
				
			||||||
	if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
 | 
						if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
 | 
				
			||||||
@ -567,7 +582,7 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Create passphrase-protected unlock key
 | 
						// Create passphrase-protected unlock key
 | 
				
			||||||
	Debug("Creating passphrase-protected unlock key")
 | 
						Debug("Creating passphrase-protected unlock key")
 | 
				
			||||||
	passphraseKey, err := CreatePassphraseKey(cli.fs, cli.stateDir, passphraseStr)
 | 
						passphraseKey, err := vault.CreatePassphraseKey(passphraseStr)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to create unlock key", "error", err)
 | 
							Debug("Failed to create unlock key", "error", err)
 | 
				
			||||||
		return fmt.Errorf("failed to create unlock key: %w", err)
 | 
							return fmt.Errorf("failed to create unlock key: %w", err)
 | 
				
			||||||
@ -749,24 +764,41 @@ func (cli *CLIInstance) VaultSelect(name string) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// AddSecret adds a secret to the vault
 | 
					// AddSecret adds a secret to the vault
 | 
				
			||||||
func (cli *CLIInstance) AddSecret(secretName string, force bool) error {
 | 
					func (cli *CLIInstance) AddSecret(secretName string, force bool) error {
 | 
				
			||||||
 | 
						Debug("CLI AddSecret starting", "secret_name", secretName, "force", force)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get current vault
 | 
						// Get current vault
 | 
				
			||||||
 | 
						Debug("Getting current vault")
 | 
				
			||||||
	vault, err := GetCurrentVault(cli.fs, cli.stateDir)
 | 
						vault, err := GetCurrentVault(cli.fs, cli.stateDir)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get current vault", "error", err)
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Got current vault", "vault_name", vault.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Read secret value from stdin
 | 
						// Read secret value from stdin
 | 
				
			||||||
 | 
						Debug("Reading secret value from stdin")
 | 
				
			||||||
	value, err := io.ReadAll(os.Stdin)
 | 
						value, err := io.ReadAll(os.Stdin)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to read secret from stdin", "error", err)
 | 
				
			||||||
		return fmt.Errorf("failed to read secret from stdin: %w", err)
 | 
							return fmt.Errorf("failed to read secret from stdin: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Read secret value from stdin", "value_length", len(value))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Remove trailing newline if present
 | 
						// Remove trailing newline if present
 | 
				
			||||||
	if len(value) > 0 && value[len(value)-1] == '\n' {
 | 
						if len(value) > 0 && value[len(value)-1] == '\n' {
 | 
				
			||||||
		value = value[:len(value)-1]
 | 
							value = value[:len(value)-1]
 | 
				
			||||||
 | 
							Debug("Removed trailing newline", "new_length", len(value))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return vault.AddSecret(secretName, value, force)
 | 
						Debug("Calling vault.AddSecret", "secret_name", secretName, "value_length", len(value), "force", force)
 | 
				
			||||||
 | 
						err = vault.AddSecret(secretName, value, force)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("vault.AddSecret failed", "error", err)
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						Debug("vault.AddSecret completed successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetSecret retrieves a secret from the vault
 | 
					// GetSecret retrieves a secret from the vault
 | 
				
			||||||
@ -777,27 +809,9 @@ func (cli *CLIInstance) GetSecret(secretName string) error {
 | 
				
			|||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get the secret object
 | 
						// Get the secret value using the vault's GetSecret method
 | 
				
			||||||
	secret, err := vault.GetSecretObject(secretName)
 | 
						// This handles the per-secret key architecture internally
 | 
				
			||||||
	if err != nil {
 | 
						value, err := vault.GetSecret(secretName)
 | 
				
			||||||
		return err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Get the value using the current unlock key (or mnemonic if available)
 | 
					 | 
				
			||||||
	var value []byte
 | 
					 | 
				
			||||||
	if os.Getenv(EnvMnemonic) != "" {
 | 
					 | 
				
			||||||
		// If mnemonic is available, GetValue can handle it without an unlock key
 | 
					 | 
				
			||||||
		value, err = secret.GetValue(nil)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// Get the current unlock key
 | 
					 | 
				
			||||||
		unlockKey, unlockErr := vault.GetCurrentUnlockKey()
 | 
					 | 
				
			||||||
		if unlockErr != nil {
 | 
					 | 
				
			||||||
			return fmt.Errorf("failed to get current unlock key: %w", unlockErr)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		value, err = secret.GetValue(unlockKey)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -1037,20 +1051,33 @@ func (cli *CLIInstance) KeysList(jsonOutput bool) error {
 | 
				
			|||||||
func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
 | 
					func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
 | 
				
			||||||
	switch keyType {
 | 
						switch keyType {
 | 
				
			||||||
	case "passphrase":
 | 
						case "passphrase":
 | 
				
			||||||
 | 
							// Get current vault
 | 
				
			||||||
 | 
							vault, err := GetCurrentVault(cli.fs, cli.stateDir)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("failed to get current vault: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Try to unlock the vault if not already unlocked
 | 
				
			||||||
 | 
							if vault.Locked() {
 | 
				
			||||||
 | 
								_, err := vault.UnlockVault()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return fmt.Errorf("failed to unlock vault: %w", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Check if passphrase is set in environment variable
 | 
							// Check if passphrase is set in environment variable
 | 
				
			||||||
		var passphraseStr string
 | 
							var passphraseStr string
 | 
				
			||||||
		if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
 | 
							if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
 | 
				
			||||||
			passphraseStr = envPassphrase
 | 
								passphraseStr = envPassphrase
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			// Use secure passphrase input with confirmation
 | 
								// Use secure passphrase input with confirmation
 | 
				
			||||||
			var err error
 | 
					 | 
				
			||||||
			passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ")
 | 
								passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ")
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				return fmt.Errorf("failed to read passphrase: %w", err)
 | 
									return fmt.Errorf("failed to read passphrase: %w", err)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		passphraseKey, err := CreatePassphraseKey(cli.fs, cli.stateDir, passphraseStr)
 | 
							passphraseKey, err := vault.CreatePassphraseKey(passphraseStr)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@ -1374,6 +1401,11 @@ func isValidAgeSecretKey(key string) bool {
 | 
				
			|||||||
// readSecurePassphrase reads a passphrase securely from the terminal without echoing
 | 
					// readSecurePassphrase reads a passphrase securely from the terminal without echoing
 | 
				
			||||||
// and prompts for confirmation. Falls back to regular input when not on a terminal.
 | 
					// and prompts for confirmation. Falls back to regular input when not on a terminal.
 | 
				
			||||||
func readSecurePassphrase(prompt string) (string, error) {
 | 
					func readSecurePassphrase(prompt string) (string, error) {
 | 
				
			||||||
 | 
						// Check if stderr is a terminal - if not, we can't prompt interactively
 | 
				
			||||||
 | 
						if !term.IsTerminal(int(syscall.Stderr)) {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("cannot prompt for passphrase: stderr is not a terminal (running in non-interactive mode)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if stdin is a terminal
 | 
						// Check if stdin is a terminal
 | 
				
			||||||
	if !term.IsTerminal(int(syscall.Stdin)) {
 | 
						if !term.IsTerminal(int(syscall.Stdin)) {
 | 
				
			||||||
		// Not a terminal (piped input, testing, etc.) - use shared line reader
 | 
							// Not a terminal (piped input, testing, etc.) - use shared line reader
 | 
				
			||||||
@ -1390,22 +1422,22 @@ func readSecurePassphrase(prompt string) (string, error) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Terminal input - use secure password reading with confirmation
 | 
						// Terminal input - use secure password reading with confirmation
 | 
				
			||||||
	fmt.Print(prompt)
 | 
						fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Read first passphrase
 | 
						// Read first passphrase
 | 
				
			||||||
	passphrase1, err := term.ReadPassword(int(syscall.Stdin))
 | 
						passphrase1, err := term.ReadPassword(int(syscall.Stdin))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", fmt.Errorf("failed to read passphrase: %w", err)
 | 
							return "", fmt.Errorf("failed to read passphrase: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fmt.Println() // Print newline since ReadPassword doesn't echo
 | 
						fmt.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Read confirmation passphrase
 | 
						// Read confirmation passphrase
 | 
				
			||||||
	fmt.Print("Confirm passphrase: ")
 | 
						fmt.Fprint(os.Stderr, "Confirm passphrase: ") // Write prompt to stderr, not stdout
 | 
				
			||||||
	passphrase2, err := term.ReadPassword(int(syscall.Stdin))
 | 
						passphrase2, err := term.ReadPassword(int(syscall.Stdin))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", fmt.Errorf("failed to read passphrase confirmation: %w", err)
 | 
							return "", fmt.Errorf("failed to read passphrase confirmation: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fmt.Println() // Print newline since ReadPassword doesn't echo
 | 
						fmt.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Compare passphrases
 | 
						// Compare passphrases
 | 
				
			||||||
	if string(passphrase1) != string(passphrase2) {
 | 
						if string(passphrase1) != string(passphrase2) {
 | 
				
			||||||
@ -1444,6 +1476,10 @@ func (cli *CLIInstance) importMnemonic(vaultName, mnemonic string) error {
 | 
				
			|||||||
		return fmt.Errorf("failed to write long-term public key: %w", err)
 | 
							return fmt.Errorf("failed to write long-term public key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get the vault instance and unlock it
 | 
				
			||||||
 | 
						vault := NewVault(cli.fs, vaultName, cli.stateDir)
 | 
				
			||||||
 | 
						vault.Unlock(ltIdentity)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get or create passphrase for unlock key
 | 
						// Get or create passphrase for unlock key
 | 
				
			||||||
	var passphraseStr string
 | 
						var passphraseStr string
 | 
				
			||||||
	if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
 | 
						if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
 | 
				
			||||||
@ -1456,38 +1492,12 @@ func (cli *CLIInstance) importMnemonic(vaultName, mnemonic string) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create passphrase-protected unlock key
 | 
						// Create passphrase-protected unlock key (vault is now unlocked)
 | 
				
			||||||
	passphraseKey, err := CreatePassphraseKey(cli.fs, cli.stateDir, passphraseStr)
 | 
						passphraseKey, err := vault.CreatePassphraseKey(passphraseStr)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to create unlock key: %w", err)
 | 
							return fmt.Errorf("failed to create unlock key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Encrypt long-term private key to the unlock key
 | 
					 | 
				
			||||||
	unlockKeyDir := passphraseKey.GetDirectory()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Read unlock key public key
 | 
					 | 
				
			||||||
	unlockPubKeyData, err := afero.ReadFile(cli.fs, filepath.Join(unlockKeyDir, "pub.age"))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return fmt.Errorf("failed to read unlock key public key: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	unlockRecipient, err := age.ParseX25519Recipient(string(unlockPubKeyData))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return fmt.Errorf("failed to parse unlock key public key: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Encrypt long-term private key to unlock key
 | 
					 | 
				
			||||||
	ltPrivKeyData := []byte(ltIdentity.String())
 | 
					 | 
				
			||||||
	encryptedLtPrivKey, err := encryptToRecipient(ltPrivKeyData, unlockRecipient)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return fmt.Errorf("failed to encrypt long-term private key: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Write encrypted long-term private key
 | 
					 | 
				
			||||||
	if err := afero.WriteFile(cli.fs, filepath.Join(unlockKeyDir, "longterm.age"), encryptedLtPrivKey, 0600); err != nil {
 | 
					 | 
				
			||||||
		return fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	fmt.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
 | 
						fmt.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
 | 
				
			||||||
	fmt.Printf("Long-term public key: %s\n", ltPubKey)
 | 
						fmt.Printf("Long-term public key: %s\n", ltPubKey)
 | 
				
			||||||
	fmt.Printf("Unlock key ID: %s\n", passphraseKey.GetMetadata().ID)
 | 
						fmt.Printf("Unlock key ID: %s\n", passphraseKey.GetMetadata().ID)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										66
									
								
								internal/secret/cli_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								internal/secret/cli_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					package secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCLIInstanceStateDir(t *testing.T) {
 | 
				
			||||||
 | 
						// Test the CLI instance state directory functionality
 | 
				
			||||||
 | 
						fs := afero.NewMemMapFs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create a test state directory
 | 
				
			||||||
 | 
						testStateDir := "/test-state-dir"
 | 
				
			||||||
 | 
						cli := NewCLIInstanceWithStateDir(fs, testStateDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if cli.GetStateDir() != testStateDir {
 | 
				
			||||||
 | 
							t.Errorf("Expected state directory %q, got %q", testStateDir, cli.GetStateDir())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCLIInstanceWithFs(t *testing.T) {
 | 
				
			||||||
 | 
						// Test creating CLI instance with custom filesystem
 | 
				
			||||||
 | 
						fs := afero.NewMemMapFs()
 | 
				
			||||||
 | 
						cli := NewCLIInstanceWithFs(fs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The state directory should be determined automatically
 | 
				
			||||||
 | 
						stateDir := cli.GetStateDir()
 | 
				
			||||||
 | 
						if stateDir == "" {
 | 
				
			||||||
 | 
							t.Error("Expected non-empty state directory")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDetermineStateDir(t *testing.T) {
 | 
				
			||||||
 | 
						// Test the determineStateDir function
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Save original environment and restore it after test
 | 
				
			||||||
 | 
						originalStateDir := os.Getenv(EnvStateDir)
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if originalStateDir == "" {
 | 
				
			||||||
 | 
								os.Unsetenv(EnvStateDir)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Setenv(EnvStateDir, originalStateDir)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test with environment variable set
 | 
				
			||||||
 | 
						testEnvDir := "/test-env-dir"
 | 
				
			||||||
 | 
						os.Setenv(EnvStateDir, testEnvDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := determineStateDir("")
 | 
				
			||||||
 | 
						if stateDir != testEnvDir {
 | 
				
			||||||
 | 
							t.Errorf("Expected state directory %q from environment, got %q", testEnvDir, stateDir)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test with custom config dir
 | 
				
			||||||
 | 
						os.Unsetenv(EnvStateDir)
 | 
				
			||||||
 | 
						customConfigDir := "/custom-config"
 | 
				
			||||||
 | 
						stateDir = determineStateDir(customConfigDir)
 | 
				
			||||||
 | 
						expectedDir := filepath.Join(customConfigDir, AppID)
 | 
				
			||||||
 | 
						if stateDir != expectedDir {
 | 
				
			||||||
 | 
							t.Errorf("Expected state directory %q with custom config, got %q", expectedDir, stateDir)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -4,6 +4,7 @@ import (
 | 
				
			|||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
	"syscall"
 | 
						"syscall"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"filippo.io/age"
 | 
						"filippo.io/age"
 | 
				
			||||||
@ -12,21 +13,34 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// encryptToRecipient encrypts data to a recipient using age
 | 
					// encryptToRecipient encrypts data to a recipient using age
 | 
				
			||||||
func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
 | 
					func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
 | 
				
			||||||
 | 
						Debug("encryptToRecipient starting", "data_length", len(data))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var buf bytes.Buffer
 | 
						var buf bytes.Buffer
 | 
				
			||||||
 | 
						Debug("Creating age encryptor")
 | 
				
			||||||
	w, err := age.Encrypt(&buf, recipient)
 | 
						w, err := age.Encrypt(&buf, recipient)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to create encryptor", "error", err)
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create encryptor: %w", err)
 | 
							return nil, fmt.Errorf("failed to create encryptor: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Created age encryptor successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Writing data to encryptor")
 | 
				
			||||||
	if _, err := w.Write(data); err != nil {
 | 
						if _, err := w.Write(data); err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to write data to encryptor", "error", err)
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write data: %w", err)
 | 
							return nil, fmt.Errorf("failed to write data: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Wrote data to encryptor successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Closing encryptor")
 | 
				
			||||||
	if err := w.Close(); err != nil {
 | 
						if err := w.Close(); err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to close encryptor", "error", err)
 | 
				
			||||||
		return nil, fmt.Errorf("failed to close encryptor: %w", err)
 | 
							return nil, fmt.Errorf("failed to close encryptor: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Closed encryptor successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return buf.Bytes(), nil
 | 
						result := buf.Bytes()
 | 
				
			||||||
 | 
						Debug("encryptToRecipient completed successfully", "result_length", len(result))
 | 
				
			||||||
 | 
						return result, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// decryptWithIdentity decrypts data with an identity using age
 | 
					// decryptWithIdentity decrypts data with an identity using age
 | 
				
			||||||
@ -67,25 +81,24 @@ func decryptWithPassphrase(encryptedData []byte, passphrase string) ([]byte, err
 | 
				
			|||||||
// readPassphrase reads a passphrase securely from the terminal without echoing
 | 
					// readPassphrase reads a passphrase securely from the terminal without echoing
 | 
				
			||||||
// This version is for unlocking and doesn't require confirmation
 | 
					// This version is for unlocking and doesn't require confirmation
 | 
				
			||||||
func readPassphrase(prompt string) (string, error) {
 | 
					func readPassphrase(prompt string) (string, error) {
 | 
				
			||||||
 | 
						// Check if stderr is a terminal - if not, we can't prompt interactively
 | 
				
			||||||
 | 
						if !term.IsTerminal(int(syscall.Stderr)) {
 | 
				
			||||||
 | 
							return "", fmt.Errorf("cannot prompt for passphrase: stderr is not a terminal (running in non-interactive mode)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if stdin is a terminal
 | 
						// Check if stdin is a terminal
 | 
				
			||||||
	if !term.IsTerminal(int(syscall.Stdin)) {
 | 
						if !term.IsTerminal(int(syscall.Stdin)) {
 | 
				
			||||||
		// Not a terminal - fall back to regular input
 | 
							// Not a terminal - use shared line reader to avoid buffering conflicts
 | 
				
			||||||
		fmt.Print(prompt)
 | 
							return readLineFromStdin(prompt)
 | 
				
			||||||
		var passphrase string
 | 
					 | 
				
			||||||
		_, err := fmt.Scanln(&passphrase)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return "", fmt.Errorf("failed to read passphrase: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return passphrase, nil
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Terminal input - use secure password reading
 | 
						// Terminal input - use secure password reading
 | 
				
			||||||
	fmt.Print(prompt)
 | 
						fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
 | 
				
			||||||
	passphrase, err := term.ReadPassword(int(syscall.Stdin))
 | 
						passphrase, err := term.ReadPassword(int(syscall.Stdin))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", fmt.Errorf("failed to read passphrase: %w", err)
 | 
							return "", fmt.Errorf("failed to read passphrase: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fmt.Println() // Print newline since ReadPassword doesn't echo
 | 
						fmt.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(passphrase) == 0 {
 | 
						if len(passphrase) == 0 {
 | 
				
			||||||
		return "", fmt.Errorf("passphrase cannot be empty")
 | 
							return "", fmt.Errorf("passphrase cannot be empty")
 | 
				
			||||||
 | 
				
			|||||||
@ -32,6 +32,9 @@ func initDebugLogging() {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Disable stderr buffering for immediate debug output when debugging is enabled
 | 
				
			||||||
 | 
						syscall.Syscall(syscall.SYS_FCNTL, os.Stderr.Fd(), syscall.F_SETFL, syscall.O_SYNC)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if STDERR is a TTY
 | 
						// Check if STDERR is a TTY
 | 
				
			||||||
	isTTY := term.IsTerminal(int(syscall.Stderr))
 | 
						isTTY := term.IsTerminal(int(syscall.Stderr))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -130,4 +133,4 @@ func (h *colorizedHandler) WithGroup(name string) slog.Handler {
 | 
				
			|||||||
	// For simplicity, return the same handler
 | 
						// For simplicity, return the same handler
 | 
				
			||||||
	// In a more complex implementation, we'd create a new handler with the group
 | 
						// In a more complex implementation, we'd create a new handler with the group
 | 
				
			||||||
	return h
 | 
						return h
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										141
									
								
								internal/secret/debug_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								internal/secret/debug_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,141 @@
 | 
				
			|||||||
 | 
					package secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"log/slog"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"syscall"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"golang.org/x/term"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDebugLogging(t *testing.T) {
 | 
				
			||||||
 | 
						// Save original GODEBUG and restore it
 | 
				
			||||||
 | 
						originalGodebug := os.Getenv("GODEBUG")
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if originalGodebug == "" {
 | 
				
			||||||
 | 
								os.Unsetenv("GODEBUG")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Setenv("GODEBUG", originalGodebug)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// Re-initialize debug system with original setting
 | 
				
			||||||
 | 
							initDebugLogging()
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name          string
 | 
				
			||||||
 | 
							godebug       string
 | 
				
			||||||
 | 
							expectEnabled bool
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:          "debug enabled",
 | 
				
			||||||
 | 
								godebug:       "berlin.sneak.pkg.secret",
 | 
				
			||||||
 | 
								expectEnabled: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:          "debug enabled with other flags",
 | 
				
			||||||
 | 
								godebug:       "other=1,berlin.sneak.pkg.secret,another=value",
 | 
				
			||||||
 | 
								expectEnabled: true,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:          "debug disabled",
 | 
				
			||||||
 | 
								godebug:       "other=1",
 | 
				
			||||||
 | 
								expectEnabled: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name:          "debug disabled empty",
 | 
				
			||||||
 | 
								godebug:       "",
 | 
				
			||||||
 | 
								expectEnabled: false,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								// Set GODEBUG
 | 
				
			||||||
 | 
								if tt.godebug == "" {
 | 
				
			||||||
 | 
									os.Unsetenv("GODEBUG")
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									os.Setenv("GODEBUG", tt.godebug)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Re-initialize debug system
 | 
				
			||||||
 | 
								initDebugLogging()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Test if debug is enabled
 | 
				
			||||||
 | 
								enabled := IsDebugEnabled()
 | 
				
			||||||
 | 
								if enabled != tt.expectEnabled {
 | 
				
			||||||
 | 
									t.Errorf("IsDebugEnabled() = %v, want %v", enabled, tt.expectEnabled)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// If debug should be enabled, test that debug output works
 | 
				
			||||||
 | 
								if tt.expectEnabled {
 | 
				
			||||||
 | 
									// Capture debug output by redirecting the colorized handler
 | 
				
			||||||
 | 
									var buf bytes.Buffer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Override the debug logger for testing
 | 
				
			||||||
 | 
									oldLogger := debugLogger
 | 
				
			||||||
 | 
									if term.IsTerminal(int(syscall.Stderr)) {
 | 
				
			||||||
 | 
										// TTY: use colorized handler with our buffer
 | 
				
			||||||
 | 
										debugLogger = slog.New(newColorizedHandler(&buf))
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										// Non-TTY: use JSON handler with our buffer
 | 
				
			||||||
 | 
										debugLogger = slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
 | 
				
			||||||
 | 
											Level: slog.LevelDebug,
 | 
				
			||||||
 | 
										}))
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Test debug output
 | 
				
			||||||
 | 
									Debug("test message", "key", "value")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Restore original logger
 | 
				
			||||||
 | 
									debugLogger = oldLogger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Check that output was generated
 | 
				
			||||||
 | 
									output := buf.String()
 | 
				
			||||||
 | 
									if !strings.Contains(output, "test message") {
 | 
				
			||||||
 | 
										t.Errorf("Debug output does not contain expected message. Got: %s", output)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDebugFunctions(t *testing.T) {
 | 
				
			||||||
 | 
						// Enable debug for testing
 | 
				
			||||||
 | 
						originalGodebug := os.Getenv("GODEBUG")
 | 
				
			||||||
 | 
						os.Setenv("GODEBUG", "berlin.sneak.pkg.secret")
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if originalGodebug == "" {
 | 
				
			||||||
 | 
								os.Unsetenv("GODEBUG")
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Setenv("GODEBUG", originalGodebug)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							initDebugLogging()
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						initDebugLogging()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !IsDebugEnabled() {
 | 
				
			||||||
 | 
							t.Skip("Debug not enabled, skipping debug function tests")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test that debug functions don't panic and can be called
 | 
				
			||||||
 | 
						t.Run("Debug", func(t *testing.T) {
 | 
				
			||||||
 | 
							Debug("test debug message")
 | 
				
			||||||
 | 
							Debug("test with args", "key", "value", "number", 42)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("DebugF", func(t *testing.T) {
 | 
				
			||||||
 | 
							DebugF("formatted message: %s %d", "test", 123)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t.Run("DebugWith", func(t *testing.T) {
 | 
				
			||||||
 | 
							DebugWith("structured message",
 | 
				
			||||||
 | 
								slog.String("string_key", "string_value"),
 | 
				
			||||||
 | 
								slog.Int("int_key", 42),
 | 
				
			||||||
 | 
								slog.Bool("bool_key", true),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -225,7 +225,7 @@ func (s *Secret) GetEncryptedData() ([]byte, error) {
 | 
				
			|||||||
		slog.String("vault_name", s.vault.Name),
 | 
							slog.String("vault_name", s.vault.Name),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	secretPath := filepath.Join(s.Directory, "secret.age")
 | 
						secretPath := filepath.Join(s.Directory, "value.age")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Debug("Reading encrypted secret file", "secret_path", secretPath)
 | 
						Debug("Reading encrypted secret file", "secret_path", secretPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -250,7 +250,7 @@ func (s *Secret) Exists() (bool, error) {
 | 
				
			|||||||
		slog.String("vault_name", s.vault.Name),
 | 
							slog.String("vault_name", s.vault.Name),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	secretPath := filepath.Join(s.Directory, "secret.age")
 | 
						secretPath := filepath.Join(s.Directory, "value.age")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Debug("Checking secret file existence", "secret_path", secretPath)
 | 
						Debug("Checking secret file existence", "secret_path", secretPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -266,4 +266,4 @@ func (s *Secret) Exists() (bool, error) {
 | 
				
			|||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return exists, nil
 | 
						return exists, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										188
									
								
								internal/secret/secret_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								internal/secret/secret_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,188 @@
 | 
				
			|||||||
 | 
					package secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"bytes"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/pkg/agehd"
 | 
				
			||||||
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPerSecretKeyFunctionality(t *testing.T) {
 | 
				
			||||||
 | 
						// Create an in-memory filesystem for testing
 | 
				
			||||||
 | 
						fs := afero.NewMemMapFs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set up test environment variables
 | 
				
			||||||
 | 
						oldMnemonic := os.Getenv(EnvMnemonic)
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if oldMnemonic == "" {
 | 
				
			||||||
 | 
								os.Unsetenv(EnvMnemonic)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Setenv(EnvMnemonic, oldMnemonic)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set test mnemonic for direct encryption/decryption
 | 
				
			||||||
 | 
						testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
 | 
				
			||||||
 | 
						os.Setenv(EnvMnemonic, testMnemonic)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set up a test vault structure
 | 
				
			||||||
 | 
						baseDir := "/test-config/berlin.sneak.pkg.secret"
 | 
				
			||||||
 | 
						stateDir := baseDir
 | 
				
			||||||
 | 
						vaultDir := filepath.Join(baseDir, "vaults.d", "test-vault")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create vault directory structure
 | 
				
			||||||
 | 
						err := fs.MkdirAll(filepath.Join(vaultDir, "secrets.d"), 0700)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create vault directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate a long-term keypair for the vault using the test mnemonic
 | 
				
			||||||
 | 
						ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to generate long-term identity: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Write long-term public key
 | 
				
			||||||
 | 
						ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
 | 
				
			||||||
 | 
						err = afero.WriteFile(
 | 
				
			||||||
 | 
							fs,
 | 
				
			||||||
 | 
							ltPubKeyPath,
 | 
				
			||||||
 | 
							[]byte(ltIdentity.Recipient().String()),
 | 
				
			||||||
 | 
							0600,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to write long-term public key: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set current vault
 | 
				
			||||||
 | 
						currentVaultPath := filepath.Join(baseDir, "currentvault")
 | 
				
			||||||
 | 
						err = afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), 0600)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to set current vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create vault instance
 | 
				
			||||||
 | 
						vault := NewVault(fs, "test-vault", stateDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test data
 | 
				
			||||||
 | 
						secretName := "test-secret"
 | 
				
			||||||
 | 
						secretValue := []byte("this is a test secret value")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test AddSecret
 | 
				
			||||||
 | 
						t.Run("AddSecret", func(t *testing.T) {
 | 
				
			||||||
 | 
							err := vault.AddSecret(secretName, secretValue, false)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("AddSecret failed: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify that all expected files were created
 | 
				
			||||||
 | 
							secretDir := filepath.Join(vaultDir, "secrets.d", secretName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Check value.age exists (the new per-secret key architecture format)
 | 
				
			||||||
 | 
							secretExists, err := afero.Exists(
 | 
				
			||||||
 | 
								fs,
 | 
				
			||||||
 | 
								filepath.Join(secretDir, "value.age"),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if err != nil || !secretExists {
 | 
				
			||||||
 | 
								t.Fatalf("value.age file was not created")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Check metadata exists
 | 
				
			||||||
 | 
							metadataExists, err := afero.Exists(
 | 
				
			||||||
 | 
								fs,
 | 
				
			||||||
 | 
								filepath.Join(secretDir, "secret-metadata.json"),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							if err != nil || !metadataExists {
 | 
				
			||||||
 | 
								t.Fatalf("secret-metadata.json file was not created")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Logf("All expected files created successfully")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test GetSecret
 | 
				
			||||||
 | 
						t.Run("GetSecret", func(t *testing.T) {
 | 
				
			||||||
 | 
							retrievedValue, err := vault.GetSecret(secretName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("GetSecret failed: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !bytes.Equal(retrievedValue, secretValue) {
 | 
				
			||||||
 | 
								t.Fatalf(
 | 
				
			||||||
 | 
									"Retrieved value doesn't match original. Expected: %s, Got: %s",
 | 
				
			||||||
 | 
									string(secretValue),
 | 
				
			||||||
 | 
									string(retrievedValue),
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Logf("Successfully retrieved secret: %s", string(retrievedValue))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test that different secrets get different keys
 | 
				
			||||||
 | 
						t.Run("DifferentSecretsGetDifferentKeys", func(t *testing.T) {
 | 
				
			||||||
 | 
							secretName2 := "test-secret-2"
 | 
				
			||||||
 | 
							secretValue2 := []byte("this is another test secret")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Add second secret
 | 
				
			||||||
 | 
							err := vault.AddSecret(secretName2, secretValue2, false)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to add second secret: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify both secrets can be retrieved correctly
 | 
				
			||||||
 | 
							value1, err := vault.GetSecret(secretName)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to retrieve first secret: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							value2, err := vault.GetSecret(secretName2)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to retrieve second secret: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !bytes.Equal(value1, secretValue) {
 | 
				
			||||||
 | 
								t.Fatalf("First secret value mismatch")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !bytes.Equal(value2, secretValue2) {
 | 
				
			||||||
 | 
								t.Fatalf("Second secret value mismatch")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							t.Logf(
 | 
				
			||||||
 | 
								"Successfully verified that different secrets have different keys",
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestSecretNameValidation(t *testing.T) {
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name  string
 | 
				
			||||||
 | 
							valid bool
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{"valid-name", true},
 | 
				
			||||||
 | 
							{"valid.name", true},
 | 
				
			||||||
 | 
							{"valid_name", true},
 | 
				
			||||||
 | 
							{"valid/path/name", true},
 | 
				
			||||||
 | 
							{"123valid", true},
 | 
				
			||||||
 | 
							{"", false},
 | 
				
			||||||
 | 
							{"Invalid-Name", false}, // uppercase not allowed
 | 
				
			||||||
 | 
							{"invalid name", false}, // space not allowed
 | 
				
			||||||
 | 
							{"invalid@name", false}, // @ not allowed
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, test := range tests {
 | 
				
			||||||
 | 
							t.Run(test.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								result := isValidSecretName(test.name)
 | 
				
			||||||
 | 
								if result != test.valid {
 | 
				
			||||||
 | 
									t.Errorf(
 | 
				
			||||||
 | 
										"isValidSecretName(%q) = %v, want %v",
 | 
				
			||||||
 | 
										test.name,
 | 
				
			||||||
 | 
										result,
 | 
				
			||||||
 | 
										test.valid,
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -6,6 +6,7 @@ import (
 | 
				
			|||||||
	"log/slog"
 | 
						"log/slog"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -46,60 +47,220 @@ type Configuration struct {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Vault represents a secrets vault
 | 
					// Vault represents a secrets vault
 | 
				
			||||||
type Vault struct {
 | 
					type Vault struct {
 | 
				
			||||||
	Name     string
 | 
						Name        string
 | 
				
			||||||
	fs       afero.Fs
 | 
						fs          afero.Fs
 | 
				
			||||||
	stateDir string
 | 
						stateDir    string
 | 
				
			||||||
 | 
						longTermKey *age.X25519Identity // In-memory long-term key when unlocked
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewVault creates a new Vault instance
 | 
					// NewVault creates a new Vault instance
 | 
				
			||||||
func NewVault(fs afero.Fs, name string, stateDir string) *Vault {
 | 
					func NewVault(fs afero.Fs, name string, stateDir string) *Vault {
 | 
				
			||||||
	return &Vault{
 | 
						return &Vault{
 | 
				
			||||||
		Name:     name,
 | 
							Name:        name,
 | 
				
			||||||
		fs:       fs,
 | 
							fs:          fs,
 | 
				
			||||||
		stateDir: stateDir,
 | 
							stateDir:    stateDir,
 | 
				
			||||||
 | 
							longTermKey: nil,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Locked returns true if the vault doesn't have a long-term key in memory
 | 
				
			||||||
 | 
					func (v *Vault) Locked() bool {
 | 
				
			||||||
 | 
						return v.longTermKey == nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Unlock sets the long-term key in memory, unlocking the vault
 | 
				
			||||||
 | 
					func (v *Vault) Unlock(key *age.X25519Identity) {
 | 
				
			||||||
 | 
						v.longTermKey = key
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetLongTermKey returns the long-term key if available in memory
 | 
				
			||||||
 | 
					func (v *Vault) GetLongTermKey() *age.X25519Identity {
 | 
				
			||||||
 | 
						return v.longTermKey
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ClearLongTermKey removes the long-term key from memory (locks the vault)
 | 
				
			||||||
 | 
					func (v *Vault) ClearLongTermKey() {
 | 
				
			||||||
 | 
						v.longTermKey = nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetOrDeriveLongTermKey gets the long-term key from memory or derives it from available sources
 | 
				
			||||||
 | 
					func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
 | 
				
			||||||
 | 
						// If we have it in memory, return it
 | 
				
			||||||
 | 
						if !v.Locked() {
 | 
				
			||||||
 | 
							return v.longTermKey, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Vault is locked, attempting to unlock", "vault_name", v.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Try to derive from environment mnemonic first
 | 
				
			||||||
 | 
						if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
 | 
				
			||||||
 | 
							Debug("Using mnemonic from environment for long-term key derivation", "vault_name", v.Name)
 | 
				
			||||||
 | 
							ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								Debug("Failed to derive long-term key from mnemonic", "error", err, "vault_name", v.Name)
 | 
				
			||||||
 | 
								return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							DebugWith("Successfully derived long-term key from mnemonic",
 | 
				
			||||||
 | 
								slog.String("vault_name", v.Name),
 | 
				
			||||||
 | 
								slog.String("public_key", ltIdentity.Recipient().String()),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return ltIdentity, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// No mnemonic available, try to use current unlock key
 | 
				
			||||||
 | 
						Debug("No mnemonic available, using current unlock key to unlock vault", "vault_name", v.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get current unlock key
 | 
				
			||||||
 | 
						unlockKey, err := v.GetCurrentUnlockKey()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get current unlock key", "error", err, "vault_name", v.Name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get current unlock key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Retrieved current unlock key for vault unlock",
 | 
				
			||||||
 | 
							slog.String("vault_name", v.Name),
 | 
				
			||||||
 | 
							slog.String("unlock_key_type", unlockKey.GetType()),
 | 
				
			||||||
 | 
							slog.String("unlock_key_id", unlockKey.GetID()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get unlock key identity
 | 
				
			||||||
 | 
						unlockIdentity, err := unlockKey.GetIdentity()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get unlock key identity", "error", err, "unlock_key_type", unlockKey.GetType())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get unlock key identity: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Read encrypted long-term private key from unlock key directory
 | 
				
			||||||
 | 
						unlockKeyDir := unlockKey.GetDirectory()
 | 
				
			||||||
 | 
						encryptedLtPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
 | 
				
			||||||
 | 
						Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						encryptedLtPrivKey, err := afero.ReadFile(v.fs, encryptedLtPrivKeyPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to read encrypted long-term private key", "error", err, "path", encryptedLtPrivKeyPath)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Read encrypted long-term private key",
 | 
				
			||||||
 | 
							slog.String("vault_name", v.Name),
 | 
				
			||||||
 | 
							slog.String("unlock_key_type", unlockKey.GetType()),
 | 
				
			||||||
 | 
							slog.Int("encrypted_length", len(encryptedLtPrivKey)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Decrypt long-term private key using unlock key
 | 
				
			||||||
 | 
						Debug("Decrypting long-term private key with unlock key", "unlock_key_type", unlockKey.GetType())
 | 
				
			||||||
 | 
						ltPrivKeyData, err := decryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to decrypt long-term private key", "error", err, "unlock_key_type", unlockKey.GetType())
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully decrypted long-term private key",
 | 
				
			||||||
 | 
							slog.String("vault_name", v.Name),
 | 
				
			||||||
 | 
							slog.String("unlock_key_type", unlockKey.GetType()),
 | 
				
			||||||
 | 
							slog.Int("decrypted_length", len(ltPrivKeyData)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Parse long-term private key
 | 
				
			||||||
 | 
						Debug("Parsing long-term private key", "vault_name", v.Name)
 | 
				
			||||||
 | 
						ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to parse long-term private key", "error", err, "vault_name", v.Name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully obtained long-term identity via unlock key",
 | 
				
			||||||
 | 
							slog.String("vault_name", v.Name),
 | 
				
			||||||
 | 
							slog.String("unlock_key_type", unlockKey.GetType()),
 | 
				
			||||||
 | 
							slog.String("public_key", ltIdentity.Recipient().String()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return ltIdentity, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// resolveVaultSymlink resolves the currentvault symlink by changing into it and getting the absolute path
 | 
					// resolveVaultSymlink resolves the currentvault symlink by changing into it and getting the absolute path
 | 
				
			||||||
func resolveVaultSymlink(fs afero.Fs, symlinkPath string) (string, error) {
 | 
					func resolveVaultSymlink(fs afero.Fs, symlinkPath string) (string, error) {
 | 
				
			||||||
 | 
						Debug("resolveVaultSymlink starting", "symlink_path", symlinkPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// For real filesystems, we can use os.Chdir and os.Getwd
 | 
						// For real filesystems, we can use os.Chdir and os.Getwd
 | 
				
			||||||
	if _, ok := fs.(*afero.OsFs); ok {
 | 
						if _, ok := fs.(*afero.OsFs); ok {
 | 
				
			||||||
 | 
							Debug("Using real filesystem symlink resolution")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Check what the symlink points to first
 | 
				
			||||||
 | 
							Debug("Checking symlink target", "symlink_path", symlinkPath)
 | 
				
			||||||
 | 
							linkTarget, err := os.Readlink(symlinkPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								Debug("Failed to read symlink target", "error", err, "symlink_path", symlinkPath)
 | 
				
			||||||
 | 
								// Maybe it's not a symlink, try reading as file
 | 
				
			||||||
 | 
								Debug("Trying to read as file instead of symlink")
 | 
				
			||||||
 | 
								targetBytes, err := os.ReadFile(symlinkPath)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									Debug("Failed to read as file", "error", err)
 | 
				
			||||||
 | 
									return "", fmt.Errorf("failed to read vault symlink or file: %w", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								linkTarget = strings.TrimSpace(string(targetBytes))
 | 
				
			||||||
 | 
								Debug("Read vault path from file", "target", linkTarget)
 | 
				
			||||||
 | 
								return linkTarget, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							Debug("Symlink points to", "target", linkTarget)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Save current directory
 | 
							// Save current directory
 | 
				
			||||||
 | 
							Debug("Getting current directory")
 | 
				
			||||||
		originalDir, err := os.Getwd()
 | 
							originalDir, err := os.Getwd()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								Debug("Failed to get current directory", "error", err)
 | 
				
			||||||
			return "", fmt.Errorf("failed to get current directory: %w", err)
 | 
								return "", fmt.Errorf("failed to get current directory: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							Debug("Got current directory", "original_dir", originalDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Change to the symlink directory
 | 
							// Change to the symlink directory
 | 
				
			||||||
 | 
							Debug("Changing to symlink directory", "symlink_path", symlinkPath)
 | 
				
			||||||
 | 
							Debug("About to call os.Chdir - this might hang if symlink is broken")
 | 
				
			||||||
		err = os.Chdir(symlinkPath)
 | 
							err = os.Chdir(symlinkPath)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								Debug("Failed to change into vault symlink", "error", err, "symlink_path", symlinkPath)
 | 
				
			||||||
			return "", fmt.Errorf("failed to change into vault symlink: %w", err)
 | 
								return "", fmt.Errorf("failed to change into vault symlink: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							Debug("Changed to symlink directory successfully - os.Chdir completed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Get absolute path of current directory
 | 
							// Get absolute path of current directory
 | 
				
			||||||
 | 
							Debug("Getting absolute path of current directory")
 | 
				
			||||||
		absolutePath, err := os.Getwd()
 | 
							absolutePath, err := os.Getwd()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								Debug("Failed to get absolute path", "error", err)
 | 
				
			||||||
			// Try to restore original directory before returning error
 | 
								// Try to restore original directory before returning error
 | 
				
			||||||
			if restoreErr := os.Chdir(originalDir); restoreErr != nil {
 | 
								if restoreErr := os.Chdir(originalDir); restoreErr != nil {
 | 
				
			||||||
 | 
									Debug("Failed to restore original directory", "restore_error", restoreErr)
 | 
				
			||||||
				return "", fmt.Errorf("failed to get absolute path: %w (and failed to restore directory: %v)", err, restoreErr)
 | 
									return "", fmt.Errorf("failed to get absolute path: %w (and failed to restore directory: %v)", err, restoreErr)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return "", fmt.Errorf("failed to get absolute path: %w", err)
 | 
								return "", fmt.Errorf("failed to get absolute path: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							Debug("Got absolute path", "absolute_path", absolutePath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Restore original directory
 | 
							// Restore original directory
 | 
				
			||||||
 | 
							Debug("Restoring original directory", "original_dir", originalDir)
 | 
				
			||||||
		err = os.Chdir(originalDir)
 | 
							err = os.Chdir(originalDir)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								Debug("Failed to restore original directory", "error", err, "original_dir", originalDir)
 | 
				
			||||||
			return "", fmt.Errorf("failed to restore original directory: %w", err)
 | 
								return "", fmt.Errorf("failed to restore original directory: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							Debug("Restored original directory successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							Debug("resolveVaultSymlink completed successfully", "result", absolutePath)
 | 
				
			||||||
		return absolutePath, nil
 | 
							return absolutePath, nil
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							Debug("Using mock filesystem fallback")
 | 
				
			||||||
		// Fallback for mock filesystems: read the path from file contents
 | 
							// Fallback for mock filesystems: read the path from file contents
 | 
				
			||||||
		targetBytes, err := afero.ReadFile(fs, symlinkPath)
 | 
							targetBytes, err := afero.ReadFile(fs, symlinkPath)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								Debug("Failed to read vault path from file", "error", err, "symlink_path", symlinkPath)
 | 
				
			||||||
			return "", fmt.Errorf("failed to read vault path: %w", err)
 | 
								return "", fmt.Errorf("failed to read vault path: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return strings.TrimSpace(string(targetBytes)), nil
 | 
							result := strings.TrimSpace(string(targetBytes))
 | 
				
			||||||
 | 
							Debug("Read vault path from file", "result", result)
 | 
				
			||||||
 | 
							return result, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -108,6 +269,7 @@ func GetCurrentVault(fs afero.Fs, stateDir string) (*Vault, error) {
 | 
				
			|||||||
	DebugWith("Getting current vault", slog.String("state_dir", stateDir))
 | 
						DebugWith("Getting current vault", slog.String("state_dir", stateDir))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	currentVaultPath := filepath.Join(stateDir, "currentvault")
 | 
						currentVaultPath := filepath.Join(stateDir, "currentvault")
 | 
				
			||||||
 | 
						Debug("Checking current vault symlink", "path", currentVaultPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if the symlink exists
 | 
						// Check if the symlink exists
 | 
				
			||||||
	_, err := fs.Stat(currentVaultPath)
 | 
						_, err := fs.Stat(currentVaultPath)
 | 
				
			||||||
@ -115,24 +277,32 @@ func GetCurrentVault(fs afero.Fs, stateDir string) (*Vault, error) {
 | 
				
			|||||||
		Debug("Failed to stat current vault symlink", "error", err, "path", currentVaultPath)
 | 
							Debug("Failed to stat current vault symlink", "error", err, "path", currentVaultPath)
 | 
				
			||||||
		return nil, fmt.Errorf("failed to read current vault symlink: %w", err)
 | 
							return nil, fmt.Errorf("failed to read current vault symlink: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Current vault symlink exists")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Resolve the symlink to get the target directory
 | 
						// Resolve the symlink to get the target directory
 | 
				
			||||||
 | 
						Debug("Resolving vault symlink")
 | 
				
			||||||
	targetPath, err := resolveVaultSymlink(fs, currentVaultPath)
 | 
						targetPath, err := resolveVaultSymlink(fs, currentVaultPath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to resolve vault symlink", "error", err, "symlink_path", currentVaultPath)
 | 
							Debug("Failed to resolve vault symlink", "error", err, "symlink_path", currentVaultPath)
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Resolved vault symlink", "target_path", targetPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Extract vault name from the target path
 | 
						// Extract vault name from the target path
 | 
				
			||||||
	// Target path should be something like "/state/vaults.d/vaultname"
 | 
						// Target path should be something like "/state/vaults.d/vaultname"
 | 
				
			||||||
	vaultName := filepath.Base(targetPath)
 | 
						vaultName := filepath.Base(targetPath)
 | 
				
			||||||
 | 
						Debug("Extracted vault name", "vault_name", vaultName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	DebugWith("Current vault resolved",
 | 
						DebugWith("Current vault resolved",
 | 
				
			||||||
		slog.String("vault_name", vaultName),
 | 
							slog.String("vault_name", vaultName),
 | 
				
			||||||
		slog.String("target_path", targetPath),
 | 
							slog.String("target_path", targetPath),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return NewVault(fs, vaultName, stateDir), nil
 | 
						Debug("Creating NewVault instance")
 | 
				
			||||||
 | 
						vault := NewVault(fs, vaultName, stateDir)
 | 
				
			||||||
 | 
						Debug("Created NewVault instance successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return vault, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ListVaults returns a list of all available vaults
 | 
					// ListVaults returns a list of all available vaults
 | 
				
			||||||
@ -259,6 +429,7 @@ func SelectVault(fs afero.Fs, stateDir string, name string) error {
 | 
				
			|||||||
			return fmt.Errorf("failed to create symlink for current vault: %w", err)
 | 
								return fmt.Errorf("failed to create symlink for current vault: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 | 
							// FIXME this code should not exist!  we do not support the currentvaultpath not being a symlink.  remove this!
 | 
				
			||||||
		Debug("Creating vault path file (symlinks not supported)", "target", vaultDir, "file", currentVaultPath)
 | 
							Debug("Creating vault path file (symlinks not supported)", "target", vaultDir, "file", currentVaultPath)
 | 
				
			||||||
		// Fallback: write the vault directory path as a regular file
 | 
							// Fallback: write the vault directory path as a regular file
 | 
				
			||||||
		if err := afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), 0600); err != nil {
 | 
							if err := afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), 0600); err != nil {
 | 
				
			||||||
@ -324,6 +495,15 @@ func (v *Vault) ListSecrets() ([]string, error) {
 | 
				
			|||||||
	return secrets, nil
 | 
						return secrets, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// isValidSecretName validates secret names according to the format [a-z0-9\.\-\_\/]+
 | 
				
			||||||
 | 
					func isValidSecretName(name string) bool {
 | 
				
			||||||
 | 
						if name == "" {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						matched, _ := regexp.MatchString(`^[a-z0-9\.\-\_\/]+$`, name)
 | 
				
			||||||
 | 
						return matched
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// AddSecret adds a secret to this vault
 | 
					// AddSecret adds a secret to this vault
 | 
				
			||||||
func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
					func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			||||||
	DebugWith("Adding secret to vault",
 | 
						DebugWith("Adding secret to vault",
 | 
				
			||||||
@ -333,11 +513,20 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
		slog.Bool("force", force),
 | 
							slog.Bool("force", force),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Validate secret name
 | 
				
			||||||
 | 
						if !isValidSecretName(name) {
 | 
				
			||||||
 | 
							Debug("Invalid secret name provided", "secret_name", name)
 | 
				
			||||||
 | 
							return fmt.Errorf("invalid secret name '%s': must match pattern [a-z0-9.\\-_/]+", name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						Debug("Secret name validation passed", "secret_name", name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Getting vault directory")
 | 
				
			||||||
	vaultDir, err := v.GetDirectory()
 | 
						vaultDir, err := v.GetDirectory()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to get vault directory for secret addition", "error", err, "vault_name", v.Name)
 | 
							Debug("Failed to get vault directory for secret addition", "error", err, "vault_name", v.Name)
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Got vault directory", "vault_dir", vaultDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Convert slashes to percent signs for storage
 | 
						// Convert slashes to percent signs for storage
 | 
				
			||||||
	storageName := strings.ReplaceAll(name, "/", "%")
 | 
						storageName := strings.ReplaceAll(name, "/", "%")
 | 
				
			||||||
@ -349,11 +538,14 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if secret already exists
 | 
						// Check if secret already exists
 | 
				
			||||||
 | 
						Debug("Checking if secret already exists", "secret_dir", secretDir)
 | 
				
			||||||
	exists, err := afero.DirExists(v.fs, secretDir)
 | 
						exists, err := afero.DirExists(v.fs, secretDir)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to check if secret exists", "error", err, "secret_dir", secretDir)
 | 
							Debug("Failed to check if secret exists", "error", err, "secret_dir", secretDir)
 | 
				
			||||||
		return fmt.Errorf("failed to check if secret exists: %w", err)
 | 
							return fmt.Errorf("failed to check if secret exists: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Secret existence check complete", "exists", exists)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if exists && !force {
 | 
						if exists && !force {
 | 
				
			||||||
		Debug("Secret already exists and force not specified", "secret_name", name, "secret_dir", secretDir)
 | 
							Debug("Secret already exists and force not specified", "secret_name", name, "secret_dir", secretDir)
 | 
				
			||||||
		return fmt.Errorf("secret %s already exists (use --force to overwrite)", name)
 | 
							return fmt.Errorf("secret %s already exists (use --force to overwrite)", name)
 | 
				
			||||||
@ -365,8 +557,56 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
		Debug("Failed to create secret directory", "error", err, "secret_dir", secretDir)
 | 
							Debug("Failed to create secret directory", "error", err, "secret_dir", secretDir)
 | 
				
			||||||
		return fmt.Errorf("failed to create secret directory: %w", err)
 | 
							return fmt.Errorf("failed to create secret directory: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Created secret directory successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get long-term public key for encryption
 | 
						// Step 1: Generate a new keypair for this secret
 | 
				
			||||||
 | 
						Debug("Generating secret-specific keypair", "secret_name", name)
 | 
				
			||||||
 | 
						secretIdentity, err := age.GenerateX25519Identity()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to generate secret keypair", "error", err, "secret_name", name)
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to generate secret keypair: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						secretPublicKey := secretIdentity.Recipient().String()
 | 
				
			||||||
 | 
						secretPrivateKey := secretIdentity.String()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Generated secret keypair",
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.String("public_key", secretPublicKey),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 2: Store the secret's public key
 | 
				
			||||||
 | 
						pubKeyPath := filepath.Join(secretDir, "pub.age")
 | 
				
			||||||
 | 
						Debug("Writing secret public key", "path", pubKeyPath)
 | 
				
			||||||
 | 
						if err := afero.WriteFile(v.fs, pubKeyPath, []byte(secretPublicKey), 0600); err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to write secret public key", "error", err, "path", pubKeyPath)
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to write secret public key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						Debug("Wrote secret public key successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 3: Encrypt the secret value to the secret's public key
 | 
				
			||||||
 | 
						Debug("Encrypting secret value to secret's public key", "secret_name", name)
 | 
				
			||||||
 | 
						encryptedValue, err := encryptToRecipient(value, secretIdentity.Recipient())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to encrypt secret value", "error", err, "secret_name", name)
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to encrypt secret value: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Secret value encrypted",
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.Int("encrypted_length", len(encryptedValue)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 4: Store the encrypted secret value as value.age
 | 
				
			||||||
 | 
						valuePath := filepath.Join(secretDir, "value.age")
 | 
				
			||||||
 | 
						Debug("Writing encrypted secret value", "path", valuePath)
 | 
				
			||||||
 | 
						if err := afero.WriteFile(v.fs, valuePath, encryptedValue, 0600); err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to write encrypted secret value", "error", err, "path", valuePath)
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to write encrypted secret value: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						Debug("Wrote encrypted secret value successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 5: Get long-term public key for encrypting the secret's private key
 | 
				
			||||||
	ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
 | 
						ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
 | 
				
			||||||
	Debug("Reading long-term public key", "path", ltPubKeyPath)
 | 
						Debug("Reading long-term public key", "path", ltPubKeyPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -375,7 +615,9 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
		Debug("Failed to read long-term public key", "error", err, "path", ltPubKeyPath)
 | 
							Debug("Failed to read long-term public key", "error", err, "path", ltPubKeyPath)
 | 
				
			||||||
		return fmt.Errorf("failed to read long-term public key: %w", err)
 | 
							return fmt.Errorf("failed to read long-term public key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Read long-term public key successfully", "key_length", len(ltPubKeyData))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Parsing long-term public key")
 | 
				
			||||||
	ltRecipient, err := age.ParseX25519Recipient(string(ltPubKeyData))
 | 
						ltRecipient, err := age.ParseX25519Recipient(string(ltPubKeyData))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to parse long-term public key", "error", err)
 | 
							Debug("Failed to parse long-term public key", "error", err)
 | 
				
			||||||
@ -384,25 +626,30 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	DebugWith("Parsed long-term public key", slog.String("recipient", ltRecipient.String()))
 | 
						DebugWith("Parsed long-term public key", slog.String("recipient", ltRecipient.String()))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Encrypt secret data
 | 
						// Step 6: Encrypt the secret's private key to the long-term public key
 | 
				
			||||||
	Debug("Encrypting secret data")
 | 
						Debug("Encrypting secret private key to long-term public key", "secret_name", name)
 | 
				
			||||||
	encryptedData, err := encryptToRecipient(value, ltRecipient)
 | 
						encryptedPrivKey, err := encryptToRecipient([]byte(secretPrivateKey), ltRecipient)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to encrypt secret", "error", err)
 | 
							Debug("Failed to encrypt secret private key", "error", err, "secret_name", name)
 | 
				
			||||||
		return fmt.Errorf("failed to encrypt secret: %w", err)
 | 
							return fmt.Errorf("failed to encrypt secret private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	DebugWith("Secret encrypted", slog.Int("encrypted_length", len(encryptedData)))
 | 
						DebugWith("Secret private key encrypted",
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.Int("encrypted_length", len(encryptedPrivKey)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Write encrypted secret
 | 
						// Step 7: Store the encrypted secret private key as priv.age
 | 
				
			||||||
	secretPath := filepath.Join(secretDir, "secret.age")
 | 
						privKeyPath := filepath.Join(secretDir, "priv.age")
 | 
				
			||||||
	Debug("Writing encrypted secret", "path", secretPath)
 | 
						Debug("Writing encrypted secret private key", "path", privKeyPath)
 | 
				
			||||||
	if err := afero.WriteFile(v.fs, secretPath, encryptedData, 0600); err != nil {
 | 
						if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, 0600); err != nil {
 | 
				
			||||||
		Debug("Failed to write encrypted secret", "error", err, "path", secretPath)
 | 
							Debug("Failed to write encrypted secret private key", "error", err, "path", privKeyPath)
 | 
				
			||||||
		return fmt.Errorf("failed to write encrypted secret: %w", err)
 | 
							return fmt.Errorf("failed to write encrypted secret private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Wrote encrypted secret private key successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create and write metadata
 | 
						// Step 8: Create and write metadata
 | 
				
			||||||
 | 
						Debug("Creating secret metadata")
 | 
				
			||||||
	now := time.Now()
 | 
						now := time.Now()
 | 
				
			||||||
	metadata := SecretMetadata{
 | 
						metadata := SecretMetadata{
 | 
				
			||||||
		Name:      name,
 | 
							Name:      name,
 | 
				
			||||||
@ -416,11 +663,13 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
		slog.Time("updated_at", metadata.UpdatedAt),
 | 
							slog.Time("updated_at", metadata.UpdatedAt),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						Debug("Marshaling secret metadata")
 | 
				
			||||||
	metadataBytes, err := json.MarshalIndent(metadata, "", "  ")
 | 
						metadataBytes, err := json.MarshalIndent(metadata, "", "  ")
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to marshal secret metadata", "error", err)
 | 
							Debug("Failed to marshal secret metadata", "error", err)
 | 
				
			||||||
		return fmt.Errorf("failed to marshal secret metadata: %w", err)
 | 
							return fmt.Errorf("failed to marshal secret metadata: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Marshaled secret metadata successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	metadataPath := filepath.Join(secretDir, "secret-metadata.json")
 | 
						metadataPath := filepath.Join(secretDir, "secret-metadata.json")
 | 
				
			||||||
	Debug("Writing secret metadata", "path", metadataPath)
 | 
						Debug("Writing secret metadata", "path", metadataPath)
 | 
				
			||||||
@ -428,8 +677,9 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
		Debug("Failed to write secret metadata", "error", err, "path", metadataPath)
 | 
							Debug("Failed to write secret metadata", "error", err, "path", metadataPath)
 | 
				
			||||||
		return fmt.Errorf("failed to write secret metadata: %w", err)
 | 
							return fmt.Errorf("failed to write secret metadata: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						Debug("Wrote secret metadata successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Debug("Successfully added secret to vault", "secret_name", name, "vault_name", v.Name)
 | 
						Debug("Successfully added secret to vault with per-secret key architecture", "secret_name", name, "vault_name", v.Name)
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -440,105 +690,161 @@ func (v *Vault) GetSecret(name string) ([]byte, error) {
 | 
				
			|||||||
		slog.String("secret_name", name),
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if we have SB_SECRET_MNEMONIC environment variable for direct decryption
 | 
						// Create a secret object to handle file access
 | 
				
			||||||
	if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
 | 
					 | 
				
			||||||
		Debug("Using mnemonic from environment for secret decryption")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Use mnemonic directly to derive long-term key
 | 
					 | 
				
			||||||
		ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			Debug("Failed to derive long-term key from environment mnemonic", "error", err)
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Create a secret object to read the encrypted data
 | 
					 | 
				
			||||||
		secret := NewSecret(v, name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Check if secret exists
 | 
					 | 
				
			||||||
		exists, err := secret.Exists()
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			Debug("Failed to check if secret exists", "error", err, "secret_name", name)
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to check if secret exists: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if !exists {
 | 
					 | 
				
			||||||
			Debug("Secret not found in vault", "secret_name", name, "vault_name", v.Name)
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("secret %s not found", name)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		Debug("Secret exists, reading encrypted data", "secret_name", name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Read encrypted secret data
 | 
					 | 
				
			||||||
		encryptedData, err := secret.GetEncryptedData()
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			Debug("Failed to get encrypted secret data", "error", err, "secret_name", name)
 | 
					 | 
				
			||||||
			return nil, err
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		DebugWith("Retrieved encrypted secret data",
 | 
					 | 
				
			||||||
			slog.String("secret_name", name),
 | 
					 | 
				
			||||||
			slog.Int("encrypted_length", len(encryptedData)),
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Decrypt secret data
 | 
					 | 
				
			||||||
		Debug("Decrypting secret with long-term key", "secret_name", name)
 | 
					 | 
				
			||||||
		decryptedData, err := decryptWithIdentity(encryptedData, ltIdentity)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			Debug("Failed to decrypt secret", "error", err, "secret_name", name)
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to decrypt secret: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		DebugWith("Successfully decrypted secret",
 | 
					 | 
				
			||||||
			slog.String("secret_name", name),
 | 
					 | 
				
			||||||
			slog.Int("decrypted_length", len(decryptedData)),
 | 
					 | 
				
			||||||
		)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return decryptedData, nil
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Debug("Using unlock key for secret decryption", "secret_name", name)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Use unlock key to decrypt the secret
 | 
					 | 
				
			||||||
	unlockKey, err := v.GetCurrentUnlockKey()
 | 
					 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		Debug("Failed to get current unlock key", "error", err, "vault_name", v.Name)
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("failed to get current unlock key: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	DebugWith("Retrieved current unlock key",
 | 
					 | 
				
			||||||
		slog.String("unlock_key_type", unlockKey.GetType()),
 | 
					 | 
				
			||||||
		slog.String("unlock_key_id", unlockKey.GetID()),
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create a secret object
 | 
					 | 
				
			||||||
	secret := NewSecret(v, name)
 | 
						secret := NewSecret(v, name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if secret exists
 | 
						// Check if secret exists
 | 
				
			||||||
	exists, err := secret.Exists()
 | 
						exists, err := secret.Exists()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to check if secret exists via unlock key", "error", err, "secret_name", name)
 | 
							Debug("Failed to check if secret exists", "error", err, "secret_name", name)
 | 
				
			||||||
		return nil, fmt.Errorf("failed to check if secret exists: %w", err)
 | 
							return nil, fmt.Errorf("failed to check if secret exists: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if !exists {
 | 
						if !exists {
 | 
				
			||||||
		Debug("Secret not found via unlock key", "secret_name", name, "vault_name", v.Name)
 | 
							Debug("Secret not found in vault", "secret_name", name, "vault_name", v.Name)
 | 
				
			||||||
		return nil, fmt.Errorf("secret %s not found", name)
 | 
							return nil, fmt.Errorf("secret %s not found", name)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Debug("Decrypting secret via unlock key", "secret_name", name, "unlock_key_type", unlockKey.GetType())
 | 
						Debug("Secret exists, proceeding with vault unlock and decryption", "secret_name", name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Let the unlock key handle decryption
 | 
						// Step 1: Unlock the vault (get long-term key in memory)
 | 
				
			||||||
	decryptedData, err := unlockKey.DecryptSecret(secret)
 | 
						longTermIdentity, err := v.UnlockVault()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt secret via unlock key", "error", err, "secret_name", name, "unlock_key_type", unlockKey.GetType())
 | 
							Debug("Failed to unlock vault", "error", err, "vault_name", v.Name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to unlock vault: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully unlocked vault",
 | 
				
			||||||
 | 
							slog.String("vault_name", v.Name),
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.String("long_term_public_key", longTermIdentity.Recipient().String()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 2: Use the unlocked vault to decrypt the secret
 | 
				
			||||||
 | 
						decryptedValue, err := v.decryptSecretWithLongTermKey(name, longTermIdentity)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to decrypt secret with long-term key", "error", err, "secret_name", name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to decrypt secret: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully decrypted secret with per-secret key architecture",
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.String("vault_name", v.Name),
 | 
				
			||||||
 | 
							slog.Int("decrypted_length", len(decryptedValue)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return decryptedValue, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// UnlockVault unlocks the vault and returns the long-term private key
 | 
				
			||||||
 | 
					func (v *Vault) UnlockVault() (*age.X25519Identity, error) {
 | 
				
			||||||
 | 
						Debug("Unlocking vault", "vault_name", v.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If vault is already unlocked, return the cached key
 | 
				
			||||||
 | 
						if !v.Locked() {
 | 
				
			||||||
 | 
							Debug("Vault already unlocked, returning cached long-term key", "vault_name", v.Name)
 | 
				
			||||||
 | 
							return v.longTermKey, nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get or derive the long-term key (but don't store it yet)
 | 
				
			||||||
 | 
						longTermIdentity, err := v.GetOrDeriveLongTermKey()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get or derive long-term key", "error", err, "vault_name", v.Name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to get long-term key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Now unlock the vault by storing the key in memory
 | 
				
			||||||
 | 
						v.Unlock(longTermIdentity)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully unlocked vault",
 | 
				
			||||||
 | 
							slog.String("vault_name", v.Name),
 | 
				
			||||||
 | 
							slog.String("public_key", longTermIdentity.Recipient().String()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return longTermIdentity, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// decryptSecretWithLongTermKey decrypts a secret using the provided long-term key
 | 
				
			||||||
 | 
					func (v *Vault) decryptSecretWithLongTermKey(name string, longTermIdentity *age.X25519Identity) ([]byte, error) {
 | 
				
			||||||
 | 
						DebugWith("Decrypting secret with long-term key",
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.String("vault_name", v.Name),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get vault and secret directories
 | 
				
			||||||
 | 
						vaultDir, err := v.GetDirectory()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to get vault directory", "error", err, "vault_name", v.Name)
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	DebugWith("Successfully decrypted secret via unlock key",
 | 
						storageName := strings.ReplaceAll(name, "/", "%")
 | 
				
			||||||
 | 
						secretDir := filepath.Join(vaultDir, "secrets.d", storageName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 1: Read the encrypted secret private key from priv.age
 | 
				
			||||||
 | 
						encryptedSecretPrivKeyPath := filepath.Join(secretDir, "priv.age")
 | 
				
			||||||
 | 
						Debug("Reading encrypted secret private key", "path", encryptedSecretPrivKeyPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						encryptedSecretPrivKey, err := afero.ReadFile(v.fs, encryptedSecretPrivKeyPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to read encrypted secret private key", "error", err, "path", encryptedSecretPrivKeyPath)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to read encrypted secret private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Read encrypted secret private key",
 | 
				
			||||||
		slog.String("secret_name", name),
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
		slog.String("unlock_key_type", unlockKey.GetType()),
 | 
							slog.Int("encrypted_length", len(encryptedSecretPrivKey)),
 | 
				
			||||||
		slog.Int("decrypted_length", len(decryptedData)),
 | 
					 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return decryptedData, nil
 | 
						// Step 2: Decrypt the secret's private key using the long-term private key
 | 
				
			||||||
 | 
						Debug("Decrypting secret private key with long-term key", "secret_name", name)
 | 
				
			||||||
 | 
						secretPrivKeyData, err := decryptWithIdentity(encryptedSecretPrivKey, longTermIdentity)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to decrypt secret private key", "error", err, "secret_name", name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to decrypt secret private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 3: Parse the secret's private key
 | 
				
			||||||
 | 
						Debug("Parsing secret private key", "secret_name", name)
 | 
				
			||||||
 | 
						secretIdentity, err := age.ParseX25519Identity(string(secretPrivKeyData))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to parse secret private key", "error", err, "secret_name", name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to parse secret private key: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully parsed secret identity",
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.String("public_key", secretIdentity.Recipient().String()),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 4: Read the encrypted secret value from value.age
 | 
				
			||||||
 | 
						encryptedValuePath := filepath.Join(secretDir, "value.age")
 | 
				
			||||||
 | 
						Debug("Reading encrypted secret value", "path", encryptedValuePath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						encryptedValue, err := afero.ReadFile(v.fs, encryptedValuePath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to read encrypted secret value", "error", err, "path", encryptedValuePath)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to read encrypted secret value: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Read encrypted secret value",
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.Int("encrypted_length", len(encryptedValue)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Step 5: Decrypt the secret value using the secret's private key
 | 
				
			||||||
 | 
						Debug("Decrypting secret value with secret's private key", "secret_name", name)
 | 
				
			||||||
 | 
						decryptedValue, err := decryptWithIdentity(encryptedValue, secretIdentity)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							Debug("Failed to decrypt secret value", "error", err, "secret_name", name)
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("failed to decrypt secret value: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						DebugWith("Successfully decrypted secret value",
 | 
				
			||||||
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
 | 
							slog.Int("decrypted_length", len(decryptedValue)),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return decryptedValue, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetSecretObject retrieves a Secret object with metadata loaded from this vault
 | 
					// GetSecretObject retrieves a Secret object with metadata loaded from this vault
 | 
				
			||||||
@ -873,7 +1179,12 @@ func (v *Vault) SelectUnlockKey(keyID string) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreatePassphraseKey creates a new passphrase-protected unlock key for this vault
 | 
					// CreatePassphraseKey creates a new passphrase-protected unlock key for this vault
 | 
				
			||||||
 | 
					// The vault must be unlocked (have a long-term key in memory) before calling this method
 | 
				
			||||||
func (v *Vault) CreatePassphraseKey(passphrase string) (*PassphraseUnlockKey, error) {
 | 
					func (v *Vault) CreatePassphraseKey(passphrase string) (*PassphraseUnlockKey, error) {
 | 
				
			||||||
 | 
						if v.Locked() {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("vault must be unlocked before creating passphrase key")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	vaultDir, err := v.GetDirectory()
 | 
						vaultDir, err := v.GetDirectory()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to get vault directory: %w", err)
 | 
							return nil, fmt.Errorf("failed to get vault directory: %w", err)
 | 
				
			||||||
@ -923,78 +1234,11 @@ func (v *Vault) CreatePassphraseKey(passphrase string) (*PassphraseUnlockKey, er
 | 
				
			|||||||
		return nil, fmt.Errorf("failed to write encrypted private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to write encrypted private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get or derive the long-term private key
 | 
						// Get the long-term private key from memory (vault must be unlocked)
 | 
				
			||||||
	var ltPrivKeyData []byte
 | 
						ltPrivKey := []byte(v.longTermKey.String())
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Check if mnemonic is available in environment variable
 | 
					 | 
				
			||||||
	if envMnemonic := os.Getenv(EnvMnemonic); envMnemonic != "" {
 | 
					 | 
				
			||||||
		// Use mnemonic directly to derive long-term key
 | 
					 | 
				
			||||||
		ltIdentity, err := agehd.DeriveIdentity(envMnemonic, 0)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to derive long-term key from mnemonic: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		ltPrivKeyData = []byte(ltIdentity.String())
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		// Try to get the long-term private key from the current unlock key
 | 
					 | 
				
			||||||
		currentUnlockKeyPath := filepath.Join(vaultDir, "current-unlock-key")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Check if current unlock key exists
 | 
					 | 
				
			||||||
		_, err := v.fs.Stat(currentUnlockKeyPath)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("no current unlock key found and no mnemonic available in environment. Set SB_SECRET_MNEMONIC or ensure a current unlock key exists")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Resolve the current unlock key path
 | 
					 | 
				
			||||||
		var currentUnlockKeyDir string
 | 
					 | 
				
			||||||
		if _, ok := v.fs.(*afero.OsFs); ok {
 | 
					 | 
				
			||||||
			// For real filesystems, resolve the symlink properly
 | 
					 | 
				
			||||||
			currentUnlockKeyDir, err = resolveVaultSymlink(v.fs, currentUnlockKeyPath)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return nil, fmt.Errorf("failed to resolve current unlock key symlink: %w", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			// Fallback for mock filesystems: read the path from file contents
 | 
					 | 
				
			||||||
			currentUnlockKeyTarget, err := afero.ReadFile(v.fs, currentUnlockKeyPath)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return nil, fmt.Errorf("failed to read current unlock key: %w", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			currentUnlockKeyDir = strings.TrimSpace(string(currentUnlockKeyTarget))
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Read the current unlock key's encrypted private key
 | 
					 | 
				
			||||||
		currentEncPrivKeyData, err := afero.ReadFile(v.fs, filepath.Join(currentUnlockKeyDir, "priv.age"))
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to read current unlock key private key: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Decrypt the current unlock key private key with the same passphrase
 | 
					 | 
				
			||||||
		// (assuming the user wants to use the same passphrase for the new key)
 | 
					 | 
				
			||||||
		currentPrivKeyData, err := decryptWithPassphrase(currentEncPrivKeyData, passphrase)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to decrypt current unlock key private key: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Parse the current unlock key
 | 
					 | 
				
			||||||
		currentIdentity, err := age.ParseX25519Identity(string(currentPrivKeyData))
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to parse current unlock key: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Read the encrypted long-term private key
 | 
					 | 
				
			||||||
		encryptedLtPrivKey, err := afero.ReadFile(v.fs, filepath.Join(currentUnlockKeyDir, "longterm.age"))
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Decrypt the long-term private key using the current unlock key
 | 
					 | 
				
			||||||
		ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentIdentity)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Encrypt the long-term private key to the new unlock key
 | 
						// Encrypt the long-term private key to the new unlock key
 | 
				
			||||||
	encryptedLtPrivKey, err := encryptToRecipient(ltPrivKeyData, identity.Recipient())
 | 
						encryptedLtPrivKey, err := encryptToRecipient(ltPrivKey, identity.Recipient())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to encrypt long-term private key to new unlock key: %w", err)
 | 
							return nil, fmt.Errorf("failed to encrypt long-term private key to new unlock key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										574
									
								
								internal/secret/vault_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										574
									
								
								internal/secret/vault_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,574 @@
 | 
				
			|||||||
 | 
					package secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"filippo.io/age"
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/pkg/agehd"
 | 
				
			||||||
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// setupTestEnvironment sets up the test environment with mock filesystem and environment variables
 | 
				
			||||||
 | 
					func setupTestEnvironment(t *testing.T) (afero.Fs, func()) {
 | 
				
			||||||
 | 
						// Create mock filesystem
 | 
				
			||||||
 | 
						fs := afero.NewMemMapFs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Save original environment variables
 | 
				
			||||||
 | 
						oldMnemonic := os.Getenv(EnvMnemonic)
 | 
				
			||||||
 | 
						oldPassphrase := os.Getenv(EnvUnlockPassphrase)
 | 
				
			||||||
 | 
						oldStateDir := os.Getenv(EnvStateDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create a real temporary directory for the state directory
 | 
				
			||||||
 | 
						// This is needed because GetStateDir checks the real filesystem
 | 
				
			||||||
 | 
						realTempDir, err := os.MkdirTemp("", "secret-test-*")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create real temp directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set test environment variables
 | 
				
			||||||
 | 
						os.Setenv(EnvMnemonic, testMnemonic)
 | 
				
			||||||
 | 
						os.Setenv(EnvUnlockPassphrase, "test-passphrase")
 | 
				
			||||||
 | 
						os.Setenv(EnvStateDir, realTempDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Also create the directory structure in the mock filesystem
 | 
				
			||||||
 | 
						err = fs.MkdirAll(realTempDir, 0700)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create test state directory in mock fs: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create vaults.d directory in both filesystems
 | 
				
			||||||
 | 
						vaultsDir := filepath.Join(realTempDir, "vaults.d")
 | 
				
			||||||
 | 
						err = os.MkdirAll(vaultsDir, 0700)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create real vaults directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						err = fs.MkdirAll(vaultsDir, 0700)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create mock vaults directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Return cleanup function
 | 
				
			||||||
 | 
						cleanup := func() {
 | 
				
			||||||
 | 
							// Clean up real temporary directory
 | 
				
			||||||
 | 
							os.RemoveAll(realTempDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Restore environment variables
 | 
				
			||||||
 | 
							if oldMnemonic == "" {
 | 
				
			||||||
 | 
								os.Unsetenv(EnvMnemonic)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Setenv(EnvMnemonic, oldMnemonic)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if oldPassphrase == "" {
 | 
				
			||||||
 | 
								os.Unsetenv(EnvUnlockPassphrase)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Setenv(EnvUnlockPassphrase, oldPassphrase)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if oldStateDir == "" {
 | 
				
			||||||
 | 
								os.Unsetenv(EnvStateDir)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Setenv(EnvStateDir, oldStateDir)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return fs, cleanup
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestCreateVault(t *testing.T) {
 | 
				
			||||||
 | 
						fs, cleanup := setupTestEnvironment(t)
 | 
				
			||||||
 | 
						defer cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := "/test-secret-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test creating a new vault
 | 
				
			||||||
 | 
						vault, err := CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if vault.Name != "test-vault" {
 | 
				
			||||||
 | 
							t.Errorf("Expected vault name 'test-vault', got '%s'", vault.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check that vault directory was created
 | 
				
			||||||
 | 
						vaultDir, err := vault.GetDirectory()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to get vault directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						exists, err := afero.DirExists(fs, vaultDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error checking vault directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							t.Errorf("Vault directory was not created")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check that subdirectories were created
 | 
				
			||||||
 | 
						secretsDir := filepath.Join(vaultDir, "secrets.d")
 | 
				
			||||||
 | 
						exists, err = afero.DirExists(fs, secretsDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error checking secrets directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							t.Errorf("Secrets directory was not created")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
 | 
				
			||||||
 | 
						exists, err = afero.DirExists(fs, unlockKeysDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error checking unlock keys directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							t.Errorf("Unlock keys directory was not created")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test creating a vault that already exists
 | 
				
			||||||
 | 
						_, err = CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected error when creating vault that already exists")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestSelectVault(t *testing.T) {
 | 
				
			||||||
 | 
						fs, cleanup := setupTestEnvironment(t)
 | 
				
			||||||
 | 
						defer cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := "/test-secret-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create a vault first
 | 
				
			||||||
 | 
						_, err := CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test selecting the vault
 | 
				
			||||||
 | 
						err = SelectVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to select vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check that currentvault symlink was created with correct target
 | 
				
			||||||
 | 
						currentVaultPath := filepath.Join(stateDir, "currentvault")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						content, err := afero.ReadFile(fs, currentVaultPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to read currentvault symlink: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						expectedPath := filepath.Join(stateDir, "vaults.d", "test-vault")
 | 
				
			||||||
 | 
						if string(content) != expectedPath {
 | 
				
			||||||
 | 
							t.Errorf("Expected currentvault to point to '%s', got '%s'", expectedPath, string(content))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test selecting a vault that doesn't exist
 | 
				
			||||||
 | 
						err = SelectVault(fs, stateDir, "nonexistent-vault")
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected error when selecting nonexistent vault")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetCurrentVault(t *testing.T) {
 | 
				
			||||||
 | 
						fs, cleanup := setupTestEnvironment(t)
 | 
				
			||||||
 | 
						defer cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := "/test-secret-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create and select a vault
 | 
				
			||||||
 | 
						_, err := CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = SelectVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to select vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting current vault
 | 
				
			||||||
 | 
						vault, err := GetCurrentVault(fs, stateDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to get current vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if vault.Name != "test-vault" {
 | 
				
			||||||
 | 
							t.Errorf("Expected current vault name 'test-vault', got '%s'", vault.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestListVaults(t *testing.T) {
 | 
				
			||||||
 | 
						fs, cleanup := setupTestEnvironment(t)
 | 
				
			||||||
 | 
						defer cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := "/test-secret-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initially no vaults
 | 
				
			||||||
 | 
						vaults, err := ListVaults(fs, stateDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to list vaults: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(vaults) != 0 {
 | 
				
			||||||
 | 
							t.Errorf("Expected no vaults initially, got %d", len(vaults))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create multiple vaults
 | 
				
			||||||
 | 
						vaultNames := []string{"vault1", "vault2", "vault3"}
 | 
				
			||||||
 | 
						for _, name := range vaultNames {
 | 
				
			||||||
 | 
							_, err := CreateVault(fs, stateDir, name)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create vault %s: %v", name, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// List vaults
 | 
				
			||||||
 | 
						vaults, err = ListVaults(fs, stateDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to list vaults: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(vaults) != len(vaultNames) {
 | 
				
			||||||
 | 
							t.Errorf("Expected %d vaults, got %d", len(vaultNames), len(vaults))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check that all created vaults are in the list
 | 
				
			||||||
 | 
						vaultMap := make(map[string]bool)
 | 
				
			||||||
 | 
						for _, vault := range vaults {
 | 
				
			||||||
 | 
							vaultMap[vault] = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, name := range vaultNames {
 | 
				
			||||||
 | 
							if !vaultMap[name] {
 | 
				
			||||||
 | 
								t.Errorf("Expected vault '%s' in list", name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestVaultGetDirectory(t *testing.T) {
 | 
				
			||||||
 | 
						fs, cleanup := setupTestEnvironment(t)
 | 
				
			||||||
 | 
						defer cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := "/test-secret-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						vault := NewVault(fs, "test-vault", stateDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dir, err := vault.GetDirectory()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to get vault directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						expectedDir := "/test-secret-state/vaults.d/test-vault"
 | 
				
			||||||
 | 
						if dir != expectedDir {
 | 
				
			||||||
 | 
							t.Errorf("Expected directory '%s', got '%s'", expectedDir, dir)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestAddSecret(t *testing.T) {
 | 
				
			||||||
 | 
						fs, cleanup := setupTestEnvironment(t)
 | 
				
			||||||
 | 
						defer cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := "/test-secret-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create vault and set up long-term key
 | 
				
			||||||
 | 
						vault, err := CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// We need to create a long-term public key for the vault
 | 
				
			||||||
 | 
						// This simulates what happens during vault initialization
 | 
				
			||||||
 | 
						err = setupVaultWithLongTermKey(fs, vault)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to setup vault with long-term key: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test adding a secret
 | 
				
			||||||
 | 
						secretName := "test-secret"
 | 
				
			||||||
 | 
						secretValue := []byte("super secret value")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = vault.AddSecret(secretName, secretValue, false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to add secret: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check that secret directory was created
 | 
				
			||||||
 | 
						vaultDir, _ := vault.GetDirectory()
 | 
				
			||||||
 | 
						secretDir := filepath.Join(vaultDir, "secrets.d", secretName)
 | 
				
			||||||
 | 
						exists, err := afero.DirExists(fs, secretDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error checking secret directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							t.Errorf("Secret directory was not created")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check that encrypted secret file exists
 | 
				
			||||||
 | 
						secretFile := filepath.Join(secretDir, "value.age")
 | 
				
			||||||
 | 
						exists, err = afero.Exists(fs, secretFile)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error checking secret file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							t.Errorf("Secret file was not created")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check that metadata file exists
 | 
				
			||||||
 | 
						metadataFile := filepath.Join(secretDir, "secret-metadata.json")
 | 
				
			||||||
 | 
						exists, err = afero.Exists(fs, metadataFile)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error checking metadata file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							t.Errorf("Metadata file was not created")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test adding a duplicate secret without force flag
 | 
				
			||||||
 | 
						err = vault.AddSecret(secretName, secretValue, false)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected error when adding duplicate secret without force flag")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test adding a duplicate secret with force flag
 | 
				
			||||||
 | 
						err = vault.AddSecret(secretName, []byte("new value"), true)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Errorf("Failed to overwrite secret with force flag: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test adding secret with slash in name (should be encoded)
 | 
				
			||||||
 | 
						err = vault.AddSecret("path/to/secret", []byte("value"), false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to add secret with slash in name: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check that the slash was encoded as percent
 | 
				
			||||||
 | 
						encodedSecretDir := filepath.Join(vaultDir, "secrets.d", "path%to%secret")
 | 
				
			||||||
 | 
						exists, err = afero.DirExists(fs, encodedSecretDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Error checking encoded secret directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							t.Errorf("Encoded secret directory was not created")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetSecret(t *testing.T) {
 | 
				
			||||||
 | 
						fs, cleanup := setupTestEnvironment(t)
 | 
				
			||||||
 | 
						defer cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := "/test-secret-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create vault and set up long-term key
 | 
				
			||||||
 | 
						vault, err := CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = setupVaultWithLongTermKey(fs, vault)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to setup vault with long-term key: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add a secret
 | 
				
			||||||
 | 
						secretName := "test-secret"
 | 
				
			||||||
 | 
						secretValue := []byte("super secret value")
 | 
				
			||||||
 | 
						err = vault.AddSecret(secretName, secretValue, false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to add secret: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting the secret (using mnemonic environment variable)
 | 
				
			||||||
 | 
						retrievedValue, err := vault.GetSecret(secretName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to get secret: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if string(retrievedValue) != string(secretValue) {
 | 
				
			||||||
 | 
							t.Errorf("Expected secret value '%s', got '%s'", string(secretValue), string(retrievedValue))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting a nonexistent secret
 | 
				
			||||||
 | 
						_, err = vault.GetSecret("nonexistent-secret")
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected error when getting nonexistent secret")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting secret with encoded name
 | 
				
			||||||
 | 
						encodedSecretName := "path/to/secret"
 | 
				
			||||||
 | 
						encodedSecretValue := []byte("encoded secret value")
 | 
				
			||||||
 | 
						err = vault.AddSecret(encodedSecretName, encodedSecretValue, false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to add encoded secret: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						retrievedEncodedValue, err := vault.GetSecret(encodedSecretName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to get encoded secret: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if string(retrievedEncodedValue) != string(encodedSecretValue) {
 | 
				
			||||||
 | 
							t.Errorf("Expected encoded secret value '%s', got '%s'", string(encodedSecretValue), string(retrievedEncodedValue))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestListSecrets(t *testing.T) {
 | 
				
			||||||
 | 
						fs, cleanup := setupTestEnvironment(t)
 | 
				
			||||||
 | 
						defer cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := "/test-secret-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create vault and set up long-term key
 | 
				
			||||||
 | 
						vault, err := CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = setupVaultWithLongTermKey(fs, vault)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to setup vault with long-term key: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initially no secrets
 | 
				
			||||||
 | 
						secrets, err := vault.ListSecrets()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to list secrets: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(secrets) != 0 {
 | 
				
			||||||
 | 
							t.Errorf("Expected no secrets initially, got %d", len(secrets))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add multiple secrets
 | 
				
			||||||
 | 
						secretNames := []string{"secret1", "secret2", "path/to/secret3"}
 | 
				
			||||||
 | 
						for _, name := range secretNames {
 | 
				
			||||||
 | 
							err := vault.AddSecret(name, []byte("value for "+name), false)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to add secret %s: %v", name, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// List secrets
 | 
				
			||||||
 | 
						secrets, err = vault.ListSecrets()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to list secrets: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(secrets) != len(secretNames) {
 | 
				
			||||||
 | 
							t.Errorf("Expected %d secrets, got %d", len(secretNames), len(secrets))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check that all added secrets are in the list (names should be decoded)
 | 
				
			||||||
 | 
						secretMap := make(map[string]bool)
 | 
				
			||||||
 | 
						for _, secret := range secrets {
 | 
				
			||||||
 | 
							secretMap[secret] = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, name := range secretNames {
 | 
				
			||||||
 | 
							if !secretMap[name] {
 | 
				
			||||||
 | 
								t.Errorf("Expected secret '%s' in list", name)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestGetSecretMetadata(t *testing.T) {
 | 
				
			||||||
 | 
						fs, cleanup := setupTestEnvironment(t)
 | 
				
			||||||
 | 
						defer cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := "/test-secret-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create vault and set up long-term key
 | 
				
			||||||
 | 
						vault, err := CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = setupVaultWithLongTermKey(fs, vault)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to setup vault with long-term key: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Add a secret
 | 
				
			||||||
 | 
						secretName := "test-secret"
 | 
				
			||||||
 | 
						secretValue := []byte("super secret value")
 | 
				
			||||||
 | 
						beforeAdd := time.Now()
 | 
				
			||||||
 | 
						err = vault.AddSecret(secretName, secretValue, false)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to add secret: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						afterAdd := time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get secret object and its metadata
 | 
				
			||||||
 | 
						secretObj, err := vault.GetSecretObject(secretName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to get secret object: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						metadata := secretObj.GetMetadata()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if metadata.Name != secretName {
 | 
				
			||||||
 | 
							t.Errorf("Expected metadata name '%s', got '%s'", secretName, metadata.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check that timestamps are reasonable
 | 
				
			||||||
 | 
						if metadata.CreatedAt.Before(beforeAdd) || metadata.CreatedAt.After(afterAdd) {
 | 
				
			||||||
 | 
							t.Errorf("CreatedAt timestamp is out of expected range")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if metadata.UpdatedAt.Before(beforeAdd) || metadata.UpdatedAt.After(afterAdd) {
 | 
				
			||||||
 | 
							t.Errorf("UpdatedAt timestamp is out of expected range")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting metadata for nonexistent secret
 | 
				
			||||||
 | 
						_, err = vault.GetSecretObject("nonexistent-secret")
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							t.Errorf("Expected error when getting secret object for nonexistent secret")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestListUnlockKeys(t *testing.T) {
 | 
				
			||||||
 | 
						fs, cleanup := setupTestEnvironment(t)
 | 
				
			||||||
 | 
						defer cleanup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						stateDir := "/test-secret-state"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create vault
 | 
				
			||||||
 | 
						vault, err := CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Initially no unlock keys
 | 
				
			||||||
 | 
						keys, err := vault.ListUnlockKeys()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to list unlock keys: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if len(keys) != 0 {
 | 
				
			||||||
 | 
							t.Errorf("Expected no unlock keys initially, got %d", len(keys))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// setupVaultWithLongTermKey sets up a vault with a long-term public key for testing
 | 
				
			||||||
 | 
					func setupVaultWithLongTermKey(fs afero.Fs, vault *Vault) error {
 | 
				
			||||||
 | 
						// This simulates what happens during vault initialization
 | 
				
			||||||
 | 
						// We derive a long-term keypair from the test mnemonic
 | 
				
			||||||
 | 
						ltIdentity, err := vault.deriveLongTermIdentity()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Store the long-term public key in the vault
 | 
				
			||||||
 | 
						vaultDir, err := vault.GetDirectory()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ltPubKey := ltIdentity.Recipient().String()
 | 
				
			||||||
 | 
						return afero.WriteFile(fs, filepath.Join(vaultDir, "pub.age"), []byte(ltPubKey), 0600)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// deriveLongTermIdentity is a helper method to derive the long-term identity for testing
 | 
				
			||||||
 | 
					func (v *Vault) deriveLongTermIdentity() (*age.X25519Identity, error) {
 | 
				
			||||||
 | 
						// Use agehd.DeriveIdentity with the test mnemonic
 | 
				
			||||||
 | 
						return agehd.DeriveIdentity(testMnemonic, 0)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -15,8 +15,12 @@ TEST_PASSPHRASE="test-passphrase-123"
 | 
				
			|||||||
TEMP_DIR="$(mktemp -d)"
 | 
					TEMP_DIR="$(mktemp -d)"
 | 
				
			||||||
SECRET_BINARY="./secret"
 | 
					SECRET_BINARY="./secret"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Enable debug output from the secret program
 | 
				
			||||||
 | 
					export GODEBUG="berlin.sneak.pkg.secret"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo -e "${BLUE}=== Secret Manager Comprehensive Test Script ===${NC}"
 | 
					echo -e "${BLUE}=== Secret Manager Comprehensive Test Script ===${NC}"
 | 
				
			||||||
echo -e "${YELLOW}Using temporary directory: $TEMP_DIR${NC}"
 | 
					echo -e "${YELLOW}Using temporary directory: $TEMP_DIR${NC}"
 | 
				
			||||||
 | 
					echo -e "${YELLOW}Debug output enabled: GODEBUG=$GODEBUG${NC}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Function to print test steps
 | 
					# Function to print test steps
 | 
				
			||||||
print_step() {
 | 
					print_step() {
 | 
				
			||||||
@ -76,6 +80,7 @@ cleanup() {
 | 
				
			|||||||
    unset SB_SECRET_STATE_DIR
 | 
					    unset SB_SECRET_STATE_DIR
 | 
				
			||||||
    unset SB_SECRET_MNEMONIC
 | 
					    unset SB_SECRET_MNEMONIC
 | 
				
			||||||
    unset SB_UNLOCK_PASSPHRASE
 | 
					    unset SB_UNLOCK_PASSPHRASE
 | 
				
			||||||
 | 
					    unset GODEBUG
 | 
				
			||||||
    echo -e "${GREEN}Cleanup complete${NC}"
 | 
					    echo -e "${GREEN}Cleanup complete${NC}"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -99,7 +104,8 @@ echo "  SB_SECRET_MNEMONIC=$TEST_MNEMONIC"
 | 
				
			|||||||
print_step "2" "Initializing secret manager (creates default vault)"
 | 
					print_step "2" "Initializing secret manager (creates default vault)"
 | 
				
			||||||
# Set passphrase for init command only
 | 
					# Set passphrase for init command only
 | 
				
			||||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
					export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
				
			||||||
if $SECRET_BINARY init > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY init"
 | 
				
			||||||
 | 
					if $SECRET_BINARY init; then
 | 
				
			||||||
    print_success "Secret manager initialized with default vault"
 | 
					    print_success "Secret manager initialized with default vault"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to initialize secret manager"
 | 
					    print_error "Failed to initialize secret manager"
 | 
				
			||||||
@ -119,7 +125,8 @@ print_step "3" "Testing vault management"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# List vaults (should show default)
 | 
					# List vaults (should show default)
 | 
				
			||||||
echo "Listing vaults..."
 | 
					echo "Listing vaults..."
 | 
				
			||||||
if $SECRET_BINARY vault list > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault list"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault list; then
 | 
				
			||||||
    VAULTS=$($SECRET_BINARY vault list)
 | 
					    VAULTS=$($SECRET_BINARY vault list)
 | 
				
			||||||
    echo "Available vaults: $VAULTS"
 | 
					    echo "Available vaults: $VAULTS"
 | 
				
			||||||
    print_success "Listed vaults successfully"
 | 
					    print_success "Listed vaults successfully"
 | 
				
			||||||
@ -129,7 +136,8 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Create a new vault
 | 
					# Create a new vault
 | 
				
			||||||
echo "Creating new vault 'work'..."
 | 
					echo "Creating new vault 'work'..."
 | 
				
			||||||
if $SECRET_BINARY vault create work > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault create work"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault create work; then
 | 
				
			||||||
    print_success "Created vault 'work'"
 | 
					    print_success "Created vault 'work'"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to create vault 'work'"
 | 
					    print_error "Failed to create vault 'work'"
 | 
				
			||||||
@ -137,7 +145,8 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Create another vault
 | 
					# Create another vault
 | 
				
			||||||
echo "Creating new vault 'personal'..."
 | 
					echo "Creating new vault 'personal'..."
 | 
				
			||||||
if $SECRET_BINARY vault create personal > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault create personal"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault create personal; then
 | 
				
			||||||
    print_success "Created vault 'personal'"
 | 
					    print_success "Created vault 'personal'"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to create vault 'personal'"
 | 
					    print_error "Failed to create vault 'personal'"
 | 
				
			||||||
@ -145,7 +154,8 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# List vaults again (should show default, work, personal)
 | 
					# List vaults again (should show default, work, personal)
 | 
				
			||||||
echo "Listing vaults after creation..."
 | 
					echo "Listing vaults after creation..."
 | 
				
			||||||
if $SECRET_BINARY vault list > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault list"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault list; then
 | 
				
			||||||
    VAULTS=$($SECRET_BINARY vault list)
 | 
					    VAULTS=$($SECRET_BINARY vault list)
 | 
				
			||||||
    echo "Available vaults: $VAULTS"
 | 
					    echo "Available vaults: $VAULTS"
 | 
				
			||||||
    print_success "Listed vaults after creation"
 | 
					    print_success "Listed vaults after creation"
 | 
				
			||||||
@ -155,7 +165,8 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Switch to work vault
 | 
					# Switch to work vault
 | 
				
			||||||
echo "Switching to 'work' vault..."
 | 
					echo "Switching to 'work' vault..."
 | 
				
			||||||
if $SECRET_BINARY vault select work > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault select work"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault select work; then
 | 
				
			||||||
    print_success "Switched to 'work' vault"
 | 
					    print_success "Switched to 'work' vault"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to switch to 'work' vault"
 | 
					    print_error "Failed to switch to 'work' vault"
 | 
				
			||||||
@ -170,7 +181,8 @@ reset_state
 | 
				
			|||||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
					export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Create a vault first
 | 
					# Create a vault first
 | 
				
			||||||
if $SECRET_BINARY vault create test-vault > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault create test-vault"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault create test-vault; then
 | 
				
			||||||
    print_success "Created test-vault for import testing"
 | 
					    print_success "Created test-vault for import testing"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to create test-vault"
 | 
					    print_error "Failed to create test-vault"
 | 
				
			||||||
@ -178,7 +190,8 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Import should prompt for passphrase
 | 
					# Import should prompt for passphrase
 | 
				
			||||||
echo "Importing with mnemonic env var set, should prompt for passphrase..."
 | 
					echo "Importing with mnemonic env var set, should prompt for passphrase..."
 | 
				
			||||||
if echo "$TEST_PASSPHRASE" | $SECRET_BINARY import test-vault > /dev/null 2>&1; then
 | 
					echo "Running: echo \"$TEST_PASSPHRASE\" | $SECRET_BINARY vault import test-vault"
 | 
				
			||||||
 | 
					if echo "$TEST_PASSPHRASE" | $SECRET_BINARY vault import test-vault; then
 | 
				
			||||||
    print_success "Import succeeded with mnemonic env var (prompted for passphrase)"
 | 
					    print_success "Import succeeded with mnemonic env var (prompted for passphrase)"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Import failed with mnemonic env var"
 | 
					    print_error "Import failed with mnemonic env var"
 | 
				
			||||||
@ -190,7 +203,8 @@ reset_state
 | 
				
			|||||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
					export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Create a vault first
 | 
					# Create a vault first
 | 
				
			||||||
if $SECRET_BINARY vault create test-vault2 > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault create test-vault2"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault create test-vault2; then
 | 
				
			||||||
    print_success "Created test-vault2 for import testing"
 | 
					    print_success "Created test-vault2 for import testing"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to create test-vault2"
 | 
					    print_error "Failed to create test-vault2"
 | 
				
			||||||
@ -198,7 +212,8 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Import should prompt for mnemonic
 | 
					# Import should prompt for mnemonic
 | 
				
			||||||
echo "Importing with passphrase env var set, should prompt for mnemonic..."
 | 
					echo "Importing with passphrase env var set, should prompt for mnemonic..."
 | 
				
			||||||
if echo "$TEST_MNEMONIC" | $SECRET_BINARY import test-vault2 > /dev/null 2>&1; then
 | 
					echo "Running: echo \"$TEST_MNEMONIC\" | $SECRET_BINARY vault import test-vault2"
 | 
				
			||||||
 | 
					if echo "$TEST_MNEMONIC" | $SECRET_BINARY vault import test-vault2; then
 | 
				
			||||||
    print_success "Import succeeded with passphrase env var (prompted for mnemonic)"
 | 
					    print_success "Import succeeded with passphrase env var (prompted for mnemonic)"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Import failed with passphrase env var"
 | 
					    print_error "Import failed with passphrase env var"
 | 
				
			||||||
@ -211,7 +226,8 @@ export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
				
			|||||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
					export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Create a vault first
 | 
					# Create a vault first
 | 
				
			||||||
if $SECRET_BINARY vault create test-vault3 > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault create test-vault3"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault create test-vault3; then
 | 
				
			||||||
    print_success "Created test-vault3 for import testing"
 | 
					    print_success "Created test-vault3 for import testing"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to create test-vault3"
 | 
					    print_error "Failed to create test-vault3"
 | 
				
			||||||
@ -219,7 +235,8 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Import should not prompt for anything
 | 
					# Import should not prompt for anything
 | 
				
			||||||
echo "Importing with both env vars set, should not prompt..."
 | 
					echo "Importing with both env vars set, should not prompt..."
 | 
				
			||||||
if $SECRET_BINARY import test-vault3 > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault import test-vault3"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault import test-vault3; then
 | 
				
			||||||
    print_success "Import succeeded with both env vars (no prompts)"
 | 
					    print_success "Import succeeded with both env vars (no prompts)"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Import failed with both env vars"
 | 
					    print_error "Import failed with both env vars"
 | 
				
			||||||
@ -230,7 +247,8 @@ echo -e "\n${YELLOW}Test 4d: Import with neither SB_SECRET_MNEMONIC nor SB_UNLOC
 | 
				
			|||||||
reset_state
 | 
					reset_state
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Create a vault first
 | 
					# Create a vault first
 | 
				
			||||||
if $SECRET_BINARY vault create test-vault4 > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault create test-vault4"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault create test-vault4; then
 | 
				
			||||||
    print_success "Created test-vault4 for import testing"
 | 
					    print_success "Created test-vault4 for import testing"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to create test-vault4"
 | 
					    print_error "Failed to create test-vault4"
 | 
				
			||||||
@ -239,7 +257,7 @@ fi
 | 
				
			|||||||
# Import should prompt for both mnemonic and passphrase
 | 
					# Import should prompt for both mnemonic and passphrase
 | 
				
			||||||
echo "Importing with neither env var set, should prompt for both..."
 | 
					echo "Importing with neither env var set, should prompt for both..."
 | 
				
			||||||
if expect -c "
 | 
					if expect -c "
 | 
				
			||||||
    spawn $SECRET_BINARY import test-vault4
 | 
					    spawn $SECRET_BINARY vault import test-vault4
 | 
				
			||||||
    expect \"Enter your BIP39 mnemonic phrase:\"
 | 
					    expect \"Enter your BIP39 mnemonic phrase:\"
 | 
				
			||||||
    send \"$TEST_MNEMONIC\n\"
 | 
					    send \"$TEST_MNEMONIC\n\"
 | 
				
			||||||
    expect \"Enter passphrase for unlock key:\"
 | 
					    expect \"Enter passphrase for unlock key:\"
 | 
				
			||||||
@ -247,7 +265,7 @@ if expect -c "
 | 
				
			|||||||
    expect \"Confirm passphrase:\"
 | 
					    expect \"Confirm passphrase:\"
 | 
				
			||||||
    send \"$TEST_PASSPHRASE\n\"
 | 
					    send \"$TEST_PASSPHRASE\n\"
 | 
				
			||||||
    expect eof
 | 
					    expect eof
 | 
				
			||||||
" > /dev/null 2>&1; then
 | 
					"; then
 | 
				
			||||||
    print_success "Import succeeded with no env vars (prompted for both)"
 | 
					    print_success "Import succeeded with no env vars (prompted for both)"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Import failed with no env vars"
 | 
					    print_error "Import failed with no env vars"
 | 
				
			||||||
@ -260,7 +278,7 @@ export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
				
			|||||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
					export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo "Importing into non-existent vault (should fail)..."
 | 
					echo "Importing into non-existent vault (should fail)..."
 | 
				
			||||||
if $SECRET_BINARY import nonexistent-vault > /dev/null 2>&1; then
 | 
					if $SECRET_BINARY vault import nonexistent-vault; then
 | 
				
			||||||
    print_error "Import should have failed for non-existent vault"
 | 
					    print_error "Import should have failed for non-existent vault"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_success "Import correctly failed for non-existent vault"
 | 
					    print_success "Import correctly failed for non-existent vault"
 | 
				
			||||||
@ -273,14 +291,15 @@ export SB_SECRET_MNEMONIC="invalid mnemonic phrase that should not work"
 | 
				
			|||||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
					export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Create a vault first
 | 
					# Create a vault first
 | 
				
			||||||
if $SECRET_BINARY vault create test-vault5 > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault create test-vault5"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault create test-vault5; then
 | 
				
			||||||
    print_success "Created test-vault5 for invalid mnemonic testing"
 | 
					    print_success "Created test-vault5 for invalid mnemonic testing"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to create test-vault5"
 | 
					    print_error "Failed to create test-vault5"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo "Importing with invalid mnemonic (should fail)..."
 | 
					echo "Importing with invalid mnemonic (should fail)..."
 | 
				
			||||||
if $SECRET_BINARY import test-vault5 > /dev/null 2>&1; then
 | 
					if $SECRET_BINARY vault import test-vault5; then
 | 
				
			||||||
    print_error "Import should have failed with invalid mnemonic"
 | 
					    print_error "Import should have failed with invalid mnemonic"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_success "Import correctly failed with invalid mnemonic"
 | 
					    print_success "Import correctly failed with invalid mnemonic"
 | 
				
			||||||
@ -294,14 +313,15 @@ export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
				
			|||||||
print_step "5" "Testing original import functionality"
 | 
					print_step "5" "Testing original import functionality"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Initialize to create default vault
 | 
					# Initialize to create default vault
 | 
				
			||||||
if (echo "$TEST_PASSPHRASE"; echo "$TEST_PASSPHRASE") | $SECRET_BINARY init > /dev/null 2>&1; then
 | 
					if (echo "$TEST_PASSPHRASE"; echo "$TEST_PASSPHRASE") | $SECRET_BINARY init; then
 | 
				
			||||||
    print_success "Initialized for Step 5 testing"
 | 
					    print_success "Initialized for Step 5 testing"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to initialize for Step 5 testing"
 | 
					    print_error "Failed to initialize for Step 5 testing"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Create work vault for import testing
 | 
					# Create work vault for import testing
 | 
				
			||||||
if $SECRET_BINARY vault create work > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault create work"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault create work; then
 | 
				
			||||||
    print_success "Created work vault for import testing"
 | 
					    print_success "Created work vault for import testing"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to create work vault"
 | 
					    print_error "Failed to create work vault"
 | 
				
			||||||
@ -309,7 +329,8 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Switch to work vault
 | 
					# Switch to work vault
 | 
				
			||||||
echo "Switching to 'work' vault..."
 | 
					echo "Switching to 'work' vault..."
 | 
				
			||||||
if $SECRET_BINARY vault select work > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault select work"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault select work; then
 | 
				
			||||||
    print_success "Switched to 'work' vault"
 | 
					    print_success "Switched to 'work' vault"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to switch to 'work' vault"
 | 
					    print_error "Failed to switch to 'work' vault"
 | 
				
			||||||
@ -319,7 +340,8 @@ fi
 | 
				
			|||||||
echo "Importing mnemonic into 'work' vault..."
 | 
					echo "Importing mnemonic into 'work' vault..."
 | 
				
			||||||
# Set passphrase for import command only
 | 
					# Set passphrase for import command only
 | 
				
			||||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
					export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
 | 
				
			||||||
if $SECRET_BINARY import work > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault import work"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault import work; then
 | 
				
			||||||
    print_success "Imported mnemonic into 'work' vault"
 | 
					    print_success "Imported mnemonic into 'work' vault"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to import mnemonic into 'work' vault"
 | 
					    print_error "Failed to import mnemonic into 'work' vault"
 | 
				
			||||||
@ -329,7 +351,8 @@ unset SB_UNLOCK_PASSPHRASE
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Switch back to default vault
 | 
					# Switch back to default vault
 | 
				
			||||||
echo "Switching back to 'default' vault..."
 | 
					echo "Switching back to 'default' vault..."
 | 
				
			||||||
if $SECRET_BINARY vault select default > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault select default"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault select default; then
 | 
				
			||||||
    print_success "Switched back to 'default' vault"
 | 
					    print_success "Switched back to 'default' vault"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to switch back to 'default' vault"
 | 
					    print_error "Failed to switch back to 'default' vault"
 | 
				
			||||||
@ -341,7 +364,8 @@ print_step "6" "Testing unlock key management"
 | 
				
			|||||||
# Create passphrase-protected unlock key
 | 
					# Create passphrase-protected unlock key
 | 
				
			||||||
echo "Creating passphrase-protected unlock key..."
 | 
					echo "Creating passphrase-protected unlock key..."
 | 
				
			||||||
# Note: This test uses stdin input instead of environment variable to test the traditional approach
 | 
					# Note: This test uses stdin input instead of environment variable to test the traditional approach
 | 
				
			||||||
if echo "$TEST_PASSPHRASE" | $SECRET_BINARY keys add passphrase > /dev/null 2>&1; then
 | 
					echo "Running: echo \"$TEST_PASSPHRASE\" | $SECRET_BINARY keys add passphrase"
 | 
				
			||||||
 | 
					if echo "$TEST_PASSPHRASE" | $SECRET_BINARY keys add passphrase; then
 | 
				
			||||||
    print_success "Created passphrase-protected unlock key"
 | 
					    print_success "Created passphrase-protected unlock key"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to create passphrase-protected unlock key"
 | 
					    print_error "Failed to create passphrase-protected unlock key"
 | 
				
			||||||
@ -349,7 +373,8 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# List unlock keys
 | 
					# List unlock keys
 | 
				
			||||||
echo "Listing unlock keys..."
 | 
					echo "Listing unlock keys..."
 | 
				
			||||||
if $SECRET_BINARY keys list > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY keys list"
 | 
				
			||||||
 | 
					if $SECRET_BINARY keys list; then
 | 
				
			||||||
    KEYS=$($SECRET_BINARY keys list)
 | 
					    KEYS=$($SECRET_BINARY keys list)
 | 
				
			||||||
    echo "Available unlock keys: $KEYS"
 | 
					    echo "Available unlock keys: $KEYS"
 | 
				
			||||||
    print_success "Listed unlock keys"
 | 
					    print_success "Listed unlock keys"
 | 
				
			||||||
@ -364,28 +389,32 @@ print_step "7" "Testing mnemonic-based secret operations (keyless)"
 | 
				
			|||||||
echo "Adding secrets using mnemonic-based long-term key..."
 | 
					echo "Adding secrets using mnemonic-based long-term key..."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test secret 1
 | 
					# Test secret 1
 | 
				
			||||||
if echo "my-super-secret-password" | $SECRET_BINARY add "database/password" > /dev/null 2>&1; then
 | 
					echo "Running: echo \"my-super-secret-password\" | $SECRET_BINARY add \"database/password\""
 | 
				
			||||||
 | 
					if echo "my-super-secret-password" | $SECRET_BINARY add "database/password"; then
 | 
				
			||||||
    print_success "Added secret: database/password"
 | 
					    print_success "Added secret: database/password"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to add secret: database/password"
 | 
					    print_error "Failed to add secret: database/password"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test secret 2
 | 
					# Test secret 2
 | 
				
			||||||
if echo "api-key-12345" | $SECRET_BINARY add "api/key" > /dev/null 2>&1; then
 | 
					echo "Running: echo \"api-key-12345\" | $SECRET_BINARY add \"api/key\""
 | 
				
			||||||
 | 
					if echo "api-key-12345" | $SECRET_BINARY add "api/key"; then
 | 
				
			||||||
    print_success "Added secret: api/key"
 | 
					    print_success "Added secret: api/key"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to add secret: api/key"
 | 
					    print_error "Failed to add secret: api/key"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test secret 3 (with path)
 | 
					# Test secret 3 (with path)
 | 
				
			||||||
if echo "ssh-private-key-content" | $SECRET_BINARY add "ssh/private-key" > /dev/null 2>&1; then
 | 
					echo "Running: echo \"ssh-private-key-content\" | $SECRET_BINARY add \"ssh/private-key\""
 | 
				
			||||||
 | 
					if echo "ssh-private-key-content" | $SECRET_BINARY add "ssh/private-key"; then
 | 
				
			||||||
    print_success "Added secret: ssh/private-key"
 | 
					    print_success "Added secret: ssh/private-key"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to add secret: ssh/private-key"
 | 
					    print_error "Failed to add secret: ssh/private-key"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test secret 4 (with dots and underscores)
 | 
					# Test secret 4 (with dots and underscores)
 | 
				
			||||||
if echo "jwt-secret-token" | $SECRET_BINARY add "app.config_jwt_secret" > /dev/null 2>&1; then
 | 
					echo "Running: echo \"jwt-secret-token\" | $SECRET_BINARY add \"app.config_jwt_secret\""
 | 
				
			||||||
 | 
					if echo "jwt-secret-token" | $SECRET_BINARY add "app.config_jwt_secret"; then
 | 
				
			||||||
    print_success "Added secret: app.config_jwt_secret"
 | 
					    print_success "Added secret: app.config_jwt_secret"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to add secret: app.config_jwt_secret"
 | 
					    print_error "Failed to add secret: app.config_jwt_secret"
 | 
				
			||||||
@ -420,7 +449,8 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# List all secrets
 | 
					# List all secrets
 | 
				
			||||||
echo "Listing all secrets..."
 | 
					echo "Listing all secrets..."
 | 
				
			||||||
if $SECRET_BINARY list > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY list"
 | 
				
			||||||
 | 
					if $SECRET_BINARY list; then
 | 
				
			||||||
    SECRETS=$($SECRET_BINARY list)
 | 
					    SECRETS=$($SECRET_BINARY list)
 | 
				
			||||||
    echo "Secrets in current vault:"
 | 
					    echo "Secrets in current vault:"
 | 
				
			||||||
    echo "$SECRETS" | while read -r secret; do
 | 
					    echo "$SECRETS" | while read -r secret; do
 | 
				
			||||||
@ -439,20 +469,25 @@ unset SB_SECRET_MNEMONIC
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Add a secret using traditional unlock key approach
 | 
					# Add a secret using traditional unlock key approach
 | 
				
			||||||
echo "Adding secret using traditional unlock key..."
 | 
					echo "Adding secret using traditional unlock key..."
 | 
				
			||||||
if echo "traditional-secret-value" | $SECRET_BINARY add "traditional/secret" > /dev/null 2>&1; then
 | 
					echo "Running: echo \"traditional-secret-value\" | $SECRET_BINARY add \"traditional/secret\""
 | 
				
			||||||
 | 
					if echo "traditional-secret-value" | $SECRET_BINARY add "traditional/secret"; then
 | 
				
			||||||
    print_success "Added secret using traditional approach: traditional/secret"
 | 
					    print_success "Added secret using traditional approach: traditional/secret"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to add secret using traditional approach"
 | 
					    print_error "Failed to add secret using traditional approach"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Retrieve secret using traditional unlock key approach
 | 
					# Retrieve secret using traditional unlock key approach
 | 
				
			||||||
RETRIEVED_TRADITIONAL=$($SECRET_BINARY get "traditional/secret" 2>/dev/null)
 | 
					echo "Retrieving secret using traditional unlock key approach..."
 | 
				
			||||||
 | 
					RETRIEVED_TRADITIONAL=$(echo "$TEST_PASSPHRASE" | $SECRET_BINARY get "traditional/secret" 2>/dev/null)
 | 
				
			||||||
if [ "$RETRIEVED_TRADITIONAL" = "traditional-secret-value" ]; then
 | 
					if [ "$RETRIEVED_TRADITIONAL" = "traditional-secret-value" ]; then
 | 
				
			||||||
    print_success "Retrieved and verified traditional secret: traditional/secret"
 | 
					    print_success "Retrieved and verified traditional secret: traditional/secret"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to retrieve or verify traditional secret"
 | 
					    print_error "Failed to retrieve or verify traditional secret"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Re-enable mnemonic for remaining tests
 | 
				
			||||||
 | 
					export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test 9: Advanced unlock key management
 | 
					# Test 9: Advanced unlock key management
 | 
				
			||||||
print_step "9" "Testing advanced unlock key management"
 | 
					print_step "9" "Testing advanced unlock key management"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -463,7 +498,8 @@ export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
				
			|||||||
echo "Testing PGP unlock key creation..."
 | 
					echo "Testing PGP unlock key creation..."
 | 
				
			||||||
if command -v gpg >/dev/null 2>&1; then
 | 
					if command -v gpg >/dev/null 2>&1; then
 | 
				
			||||||
    # This would require a GPG key ID - for testing we'll just check the command exists
 | 
					    # This would require a GPG key ID - for testing we'll just check the command exists
 | 
				
			||||||
    if $SECRET_BINARY keys add pgp --help > /dev/null 2>&1; then
 | 
					    echo "Running: $SECRET_BINARY keys add pgp --help"
 | 
				
			||||||
 | 
					    if $SECRET_BINARY keys add pgp --help; then
 | 
				
			||||||
        print_success "PGP unlock key command available"
 | 
					        print_success "PGP unlock key command available"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_warning "PGP unlock key command not yet implemented"
 | 
					        print_warning "PGP unlock key command not yet implemented"
 | 
				
			||||||
@ -475,7 +511,8 @@ fi
 | 
				
			|||||||
# Test Secure Enclave (macOS only)
 | 
					# Test Secure Enclave (macOS only)
 | 
				
			||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
 | 
					if [[ "$OSTYPE" == "darwin"* ]]; then
 | 
				
			||||||
    echo "Testing Secure Enclave unlock key creation..."
 | 
					    echo "Testing Secure Enclave unlock key creation..."
 | 
				
			||||||
    if $SECRET_BINARY enroll sep > /dev/null 2>&1; then
 | 
					    echo "Running: $SECRET_BINARY enroll sep"
 | 
				
			||||||
 | 
					    if $SECRET_BINARY enroll sep; then
 | 
				
			||||||
        print_success "Created Secure Enclave unlock key"
 | 
					        print_success "Created Secure Enclave unlock key"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_warning "Secure Enclave unlock key creation not yet implemented"
 | 
					        print_warning "Secure Enclave unlock key creation not yet implemented"
 | 
				
			||||||
@ -486,14 +523,16 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Get current unlock key ID for testing
 | 
					# Get current unlock key ID for testing
 | 
				
			||||||
echo "Getting current unlock key for testing..."
 | 
					echo "Getting current unlock key for testing..."
 | 
				
			||||||
if $SECRET_BINARY keys list > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY keys list"
 | 
				
			||||||
 | 
					if $SECRET_BINARY keys list; then
 | 
				
			||||||
    CURRENT_KEY_ID=$($SECRET_BINARY keys list | head -n1 | awk '{print $1}')
 | 
					    CURRENT_KEY_ID=$($SECRET_BINARY keys list | head -n1 | awk '{print $1}')
 | 
				
			||||||
    if [ -n "$CURRENT_KEY_ID" ]; then
 | 
					    if [ -n "$CURRENT_KEY_ID" ]; then
 | 
				
			||||||
        print_success "Found unlock key ID: $CURRENT_KEY_ID"
 | 
					        print_success "Found unlock key ID: $CURRENT_KEY_ID"
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Test key selection
 | 
					        # Test key selection
 | 
				
			||||||
        echo "Testing unlock key selection..."
 | 
					        echo "Testing unlock key selection..."
 | 
				
			||||||
        if $SECRET_BINARY key select "$CURRENT_KEY_ID" > /dev/null 2>&1; then
 | 
					        echo "Running: $SECRET_BINARY key select $CURRENT_KEY_ID"
 | 
				
			||||||
 | 
					        if $SECRET_BINARY key select "$CURRENT_KEY_ID"; then
 | 
				
			||||||
            print_success "Selected unlock key: $CURRENT_KEY_ID"
 | 
					            print_success "Selected unlock key: $CURRENT_KEY_ID"
 | 
				
			||||||
        else
 | 
					        else
 | 
				
			||||||
            print_warning "Unlock key selection not yet implemented"
 | 
					            print_warning "Unlock key selection not yet implemented"
 | 
				
			||||||
@ -507,7 +546,8 @@ print_step "10" "Testing secret name validation and edge cases"
 | 
				
			|||||||
# Test valid names
 | 
					# Test valid names
 | 
				
			||||||
VALID_NAMES=("valid-name" "valid.name" "valid_name" "valid/path/name" "123valid" "a" "very-long-name-with-many-parts/and/paths")
 | 
					VALID_NAMES=("valid-name" "valid.name" "valid_name" "valid/path/name" "123valid" "a" "very-long-name-with-many-parts/and/paths")
 | 
				
			||||||
for name in "${VALID_NAMES[@]}"; do
 | 
					for name in "${VALID_NAMES[@]}"; do
 | 
				
			||||||
    if echo "test-value" | $SECRET_BINARY add "$name" --force > /dev/null 2>&1; then
 | 
					    echo "Running: echo \"test-value\" | $SECRET_BINARY add $name --force"
 | 
				
			||||||
 | 
					    if echo "test-value" | $SECRET_BINARY add "$name" --force; then
 | 
				
			||||||
        print_success "Valid name accepted: $name"
 | 
					        print_success "Valid name accepted: $name"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_error "Valid name rejected: $name"
 | 
					        print_error "Valid name rejected: $name"
 | 
				
			||||||
@ -518,7 +558,8 @@ done
 | 
				
			|||||||
echo "Testing invalid names (should fail)..."
 | 
					echo "Testing invalid names (should fail)..."
 | 
				
			||||||
INVALID_NAMES=("Invalid-Name" "invalid name" "invalid@name" "invalid#name" "invalid%name" "")
 | 
					INVALID_NAMES=("Invalid-Name" "invalid name" "invalid@name" "invalid#name" "invalid%name" "")
 | 
				
			||||||
for name in "${INVALID_NAMES[@]}"; do
 | 
					for name in "${INVALID_NAMES[@]}"; do
 | 
				
			||||||
    if echo "test-value" | $SECRET_BINARY add "$name" > /dev/null 2>&1; then
 | 
					    echo "Running: echo \"test-value\" | $SECRET_BINARY add $name"
 | 
				
			||||||
 | 
					    if echo "test-value" | $SECRET_BINARY add "$name"; then
 | 
				
			||||||
        print_error "Invalid name accepted (should have been rejected): '$name'"
 | 
					        print_error "Invalid name accepted (should have been rejected): '$name'"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_success "Invalid name correctly rejected: '$name'"
 | 
					        print_success "Invalid name correctly rejected: '$name'"
 | 
				
			||||||
@ -529,14 +570,16 @@ done
 | 
				
			|||||||
print_step "11" "Testing overwrite protection and force flag"
 | 
					print_step "11" "Testing overwrite protection and force flag"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Try to add existing secret without --force (should fail)
 | 
					# Try to add existing secret without --force (should fail)
 | 
				
			||||||
if echo "new-value" | $SECRET_BINARY add "database/password" > /dev/null 2>&1; then
 | 
					echo "Running: echo \"new-value\" | $SECRET_BINARY add \"database/password\""
 | 
				
			||||||
 | 
					if echo "new-value" | $SECRET_BINARY add "database/password"; then
 | 
				
			||||||
    print_error "Overwrite protection failed - secret was overwritten without --force"
 | 
					    print_error "Overwrite protection failed - secret was overwritten without --force"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_success "Overwrite protection working - secret not overwritten without --force"
 | 
					    print_success "Overwrite protection working - secret not overwritten without --force"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Try to add existing secret with --force (should succeed)
 | 
					# Try to add existing secret with --force (should succeed)
 | 
				
			||||||
if echo "new-password-value" | $SECRET_BINARY add "database/password" --force > /dev/null 2>&1; then
 | 
					echo "Running: echo \"new-password-value\" | $SECRET_BINARY add \"database/password\" --force"
 | 
				
			||||||
 | 
					if echo "new-password-value" | $SECRET_BINARY add "database/password" --force; then
 | 
				
			||||||
    print_success "Force overwrite working - secret overwritten with --force"
 | 
					    print_success "Force overwrite working - secret overwritten with --force"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Verify the new value
 | 
					    # Verify the new value
 | 
				
			||||||
@ -555,18 +598,21 @@ print_step "12" "Testing cross-vault operations"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Switch to work vault and add secrets there
 | 
					# Switch to work vault and add secrets there
 | 
				
			||||||
echo "Switching to 'work' vault for cross-vault testing..."
 | 
					echo "Switching to 'work' vault for cross-vault testing..."
 | 
				
			||||||
if $SECRET_BINARY vault select work > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault select work"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault select work; then
 | 
				
			||||||
    print_success "Switched to 'work' vault"
 | 
					    print_success "Switched to 'work' vault"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Add work-specific secrets
 | 
					    # Add work-specific secrets
 | 
				
			||||||
    if echo "work-database-password" | $SECRET_BINARY add "work/database" > /dev/null 2>&1; then
 | 
					    echo "Running: echo \"work-database-password\" | $SECRET_BINARY add \"work/database\""
 | 
				
			||||||
 | 
					    if echo "work-database-password" | $SECRET_BINARY add "work/database"; then
 | 
				
			||||||
        print_success "Added work-specific secret"
 | 
					        print_success "Added work-specific secret"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_error "Failed to add work-specific secret"
 | 
					        print_error "Failed to add work-specific secret"
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # List secrets in work vault
 | 
					    # List secrets in work vault
 | 
				
			||||||
    if $SECRET_BINARY list > /dev/null 2>&1; then
 | 
					    echo "Running: $SECRET_BINARY list"
 | 
				
			||||||
 | 
					    if $SECRET_BINARY list; then
 | 
				
			||||||
        WORK_SECRETS=$($SECRET_BINARY list)
 | 
					        WORK_SECRETS=$($SECRET_BINARY list)
 | 
				
			||||||
        echo "Secrets in work vault: $WORK_SECRETS"
 | 
					        echo "Secrets in work vault: $WORK_SECRETS"
 | 
				
			||||||
        print_success "Listed work vault secrets"
 | 
					        print_success "Listed work vault secrets"
 | 
				
			||||||
@ -579,11 +625,13 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Switch back to default vault
 | 
					# Switch back to default vault
 | 
				
			||||||
echo "Switching back to 'default' vault..."
 | 
					echo "Switching back to 'default' vault..."
 | 
				
			||||||
if $SECRET_BINARY vault select default > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault select default"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault select default; then
 | 
				
			||||||
    print_success "Switched back to 'default' vault"
 | 
					    print_success "Switched back to 'default' vault"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Verify default vault secrets are still there
 | 
					    # Verify default vault secrets are still there
 | 
				
			||||||
    if $SECRET_BINARY get "database/password" > /dev/null 2>&1; then
 | 
					    echo "Running: $SECRET_BINARY get \"database/password\""
 | 
				
			||||||
 | 
					    if $SECRET_BINARY get "database/password"; then
 | 
				
			||||||
        print_success "Default vault secrets still accessible"
 | 
					        print_success "Default vault secrets still accessible"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_error "Default vault secrets not accessible"
 | 
					        print_error "Default vault secrets not accessible"
 | 
				
			||||||
@ -645,15 +693,17 @@ fi
 | 
				
			|||||||
print_step "14" "Testing environment variable error handling"
 | 
					print_step "14" "Testing environment variable error handling"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test with non-existent state directory
 | 
					# Test with non-existent state directory
 | 
				
			||||||
export SB_SECRET_STATE_DIR="/nonexistent/directory"
 | 
					export SB_SECRET_STATE_DIR="$TEMP_DIR/nonexistent/directory"
 | 
				
			||||||
if $SECRET_BINARY get "database/password" > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY get \"database/password\""
 | 
				
			||||||
 | 
					if $SECRET_BINARY get "database/password"; then
 | 
				
			||||||
    print_error "Should have failed with non-existent state directory"
 | 
					    print_error "Should have failed with non-existent state directory"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_success "Correctly failed with non-existent state directory"
 | 
					    print_success "Correctly failed with non-existent state directory"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test init with non-existent directory (should work)
 | 
					# Test init with non-existent directory (should work)
 | 
				
			||||||
if $SECRET_BINARY init > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY init"
 | 
				
			||||||
 | 
					if $SECRET_BINARY init; then
 | 
				
			||||||
    print_success "Init works with non-existent state directory"
 | 
					    print_success "Init works with non-existent state directory"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Init should work with non-existent state directory"
 | 
					    print_error "Init should work with non-existent state directory"
 | 
				
			||||||
@ -671,15 +721,18 @@ export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
				
			|||||||
# Create another unlock key for testing removal
 | 
					# Create another unlock key for testing removal
 | 
				
			||||||
echo "Creating additional unlock key for removal testing..."
 | 
					echo "Creating additional unlock key for removal testing..."
 | 
				
			||||||
# Use stdin input instead of environment variable
 | 
					# Use stdin input instead of environment variable
 | 
				
			||||||
if echo "another-passphrase" | $SECRET_BINARY keys add passphrase > /dev/null 2>&1; then
 | 
					echo "Running: echo \"another-passphrase\" | $SECRET_BINARY keys add passphrase"
 | 
				
			||||||
 | 
					if echo "another-passphrase" | $SECRET_BINARY keys add passphrase; then
 | 
				
			||||||
    print_success "Created additional unlock key"
 | 
					    print_success "Created additional unlock key"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Get the key ID and try to remove it
 | 
					    # Get the key ID and try to remove it
 | 
				
			||||||
    if $SECRET_BINARY keys list > /dev/null 2>&1; then
 | 
					    echo "Running: $SECRET_BINARY keys list"
 | 
				
			||||||
 | 
					    if $SECRET_BINARY keys list; then
 | 
				
			||||||
        KEY_TO_REMOVE=$($SECRET_BINARY keys list | tail -n1 | awk '{print $1}')
 | 
					        KEY_TO_REMOVE=$($SECRET_BINARY keys list | tail -n1 | awk '{print $1}')
 | 
				
			||||||
        if [ -n "$KEY_TO_REMOVE" ]; then
 | 
					        if [ -n "$KEY_TO_REMOVE" ]; then
 | 
				
			||||||
            echo "Attempting to remove unlock key: $KEY_TO_REMOVE"
 | 
					            echo "Attempting to remove unlock key: $KEY_TO_REMOVE"
 | 
				
			||||||
            if $SECRET_BINARY keys rm "$KEY_TO_REMOVE" > /dev/null 2>&1; then
 | 
					            echo "Running: $SECRET_BINARY keys rm $KEY_TO_REMOVE"
 | 
				
			||||||
 | 
					            if $SECRET_BINARY keys rm "$KEY_TO_REMOVE"; then
 | 
				
			||||||
                print_success "Removed unlock key: $KEY_TO_REMOVE"
 | 
					                print_success "Removed unlock key: $KEY_TO_REMOVE"
 | 
				
			||||||
            else
 | 
					            else
 | 
				
			||||||
                print_warning "Unlock key removal not yet implemented"
 | 
					                print_warning "Unlock key removal not yet implemented"
 | 
				
			||||||
@ -703,7 +756,9 @@ fi
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Test without mnemonic but with unlock key
 | 
					# Test without mnemonic but with unlock key
 | 
				
			||||||
unset SB_SECRET_MNEMONIC
 | 
					unset SB_SECRET_MNEMONIC
 | 
				
			||||||
if $SECRET_BINARY get "database/password" > /dev/null 2>&1; then
 | 
					echo "Testing traditional unlock key access to mnemonic-created secrets..."
 | 
				
			||||||
 | 
					echo "Running: echo \"$TEST_PASSPHRASE\" | $SECRET_BINARY get \"database/password\""
 | 
				
			||||||
 | 
					if echo "$TEST_PASSPHRASE" | $SECRET_BINARY get "database/password"; then
 | 
				
			||||||
    print_success "Traditional unlock key can access mnemonic-created secrets"
 | 
					    print_success "Traditional unlock key can access mnemonic-created secrets"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_warning "Traditional unlock key cannot access mnemonic-created secrets (may need implementation)"
 | 
					    print_warning "Traditional unlock key cannot access mnemonic-created secrets (may need implementation)"
 | 
				
			||||||
@ -717,11 +772,13 @@ print_step "17" "Testing refactored architecture - separation of concerns"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
echo "Testing that secrets handle their own data access..."
 | 
					echo "Testing that secrets handle their own data access..."
 | 
				
			||||||
# Create a test secret first
 | 
					# Create a test secret first
 | 
				
			||||||
if echo "test-self-access" | $SECRET_BINARY add "test/self-access" > /dev/null 2>&1; then
 | 
					echo "Running: echo \"test-self-access\" | $SECRET_BINARY add \"test/self-access\""
 | 
				
			||||||
 | 
					if echo "test-self-access" | $SECRET_BINARY add "test/self-access"; then
 | 
				
			||||||
    print_success "Created test secret for self-access testing"
 | 
					    print_success "Created test secret for self-access testing"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Try to retrieve it (this tests that Secret.GetEncryptedData() works)
 | 
					    # Try to retrieve it (this tests that Secret.GetEncryptedData() works)
 | 
				
			||||||
    if $SECRET_BINARY get "test/self-access" > /dev/null 2>&1; then
 | 
					    echo "Running: $SECRET_BINARY get \"test/self-access\""
 | 
				
			||||||
 | 
					    if $SECRET_BINARY get "test/self-access"; then
 | 
				
			||||||
        print_success "Secret correctly handles its own data access"
 | 
					        print_success "Secret correctly handles its own data access"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_error "Secret failed to handle its own data access"
 | 
					        print_error "Secret failed to handle its own data access"
 | 
				
			||||||
@ -733,7 +790,8 @@ fi
 | 
				
			|||||||
echo "Testing unlock key delegation pattern..."
 | 
					echo "Testing unlock key delegation pattern..."
 | 
				
			||||||
# Test that vault delegates to unlock keys for decryption
 | 
					# Test that vault delegates to unlock keys for decryption
 | 
				
			||||||
# This is tested implicitly by all our secret retrieval operations
 | 
					# This is tested implicitly by all our secret retrieval operations
 | 
				
			||||||
if $SECRET_BINARY get "database/password" > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY get \"database/password\""
 | 
				
			||||||
 | 
					if $SECRET_BINARY get "database/password"; then
 | 
				
			||||||
    print_success "Vault correctly delegates to unlock keys for decryption"
 | 
					    print_success "Vault correctly delegates to unlock keys for decryption"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Vault delegation pattern failed"
 | 
					    print_error "Vault delegation pattern failed"
 | 
				
			||||||
@ -746,12 +804,15 @@ echo "Verifying all unlock key types implement required methods..."
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Create different types of unlock keys to test interface compliance
 | 
					# Create different types of unlock keys to test interface compliance
 | 
				
			||||||
echo "Testing PassphraseUnlockKey interface compliance..."
 | 
					echo "Testing PassphraseUnlockKey interface compliance..."
 | 
				
			||||||
if echo "interface-test-pass" | $SECRET_BINARY keys add passphrase > /dev/null 2>&1; then
 | 
					echo "Running: echo \"interface-test-pass\" | $SECRET_BINARY keys add passphrase"
 | 
				
			||||||
 | 
					if echo "interface-test-pass" | $SECRET_BINARY keys add passphrase; then
 | 
				
			||||||
    print_success "PassphraseUnlockKey created successfully"
 | 
					    print_success "PassphraseUnlockKey created successfully"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Test that we can use it (this verifies GetIdentity and DecryptSecret work)
 | 
					    # Test that we can use it (this verifies GetIdentity and DecryptSecret work)
 | 
				
			||||||
    if echo "interface-test-secret" | $SECRET_BINARY add "interface/test" > /dev/null 2>&1; then
 | 
					    echo "Running: echo \"interface-test-secret\" | $SECRET_BINARY add \"interface/test\""
 | 
				
			||||||
        if $SECRET_BINARY get "interface/test" > /dev/null 2>&1; then
 | 
					    if echo "interface-test-secret" | $SECRET_BINARY add "interface/test"; then
 | 
				
			||||||
 | 
					        echo "Running: $SECRET_BINARY get \"interface/test\""
 | 
				
			||||||
 | 
					        if $SECRET_BINARY get "interface/test"; then
 | 
				
			||||||
            print_success "PassphraseUnlockKey interface methods working"
 | 
					            print_success "PassphraseUnlockKey interface methods working"
 | 
				
			||||||
        else
 | 
					        else
 | 
				
			||||||
            print_error "PassphraseUnlockKey interface methods failed"
 | 
					            print_error "PassphraseUnlockKey interface methods failed"
 | 
				
			||||||
@ -766,12 +827,15 @@ fi
 | 
				
			|||||||
# Test Secure Enclave on macOS (if available)
 | 
					# Test Secure Enclave on macOS (if available)
 | 
				
			||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
 | 
					if [[ "$OSTYPE" == "darwin"* ]]; then
 | 
				
			||||||
    echo "Testing SEPUnlockKey interface compliance on macOS..."
 | 
					    echo "Testing SEPUnlockKey interface compliance on macOS..."
 | 
				
			||||||
    if $SECRET_BINARY enroll sep > /dev/null 2>&1; then
 | 
					    echo "Running: $SECRET_BINARY enroll sep"
 | 
				
			||||||
 | 
					    if $SECRET_BINARY enroll sep; then
 | 
				
			||||||
        print_success "SEPUnlockKey created successfully"
 | 
					        print_success "SEPUnlockKey created successfully"
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Test that we can use it
 | 
					        # Test that we can use it
 | 
				
			||||||
        if echo "sep-test-secret" | $SECRET_BINARY add "sep/test" > /dev/null 2>&1; then
 | 
					        echo "Running: echo \"sep-test-secret\" | $SECRET_BINARY add \"sep/test\""
 | 
				
			||||||
            if $SECRET_BINARY get "sep/test" > /dev/null 2>&1; then
 | 
					        if echo "sep-test-secret" | $SECRET_BINARY add "sep/test"; then
 | 
				
			||||||
 | 
					            echo "Running: $SECRET_BINARY get \"sep/test\""
 | 
				
			||||||
 | 
					            if $SECRET_BINARY get "sep/test"; then
 | 
				
			||||||
                print_success "SEPUnlockKey interface methods working"
 | 
					                print_success "SEPUnlockKey interface methods working"
 | 
				
			||||||
            else
 | 
					            else
 | 
				
			||||||
                print_error "SEPUnlockKey interface methods failed"
 | 
					                print_error "SEPUnlockKey interface methods failed"
 | 
				
			||||||
@ -787,36 +851,40 @@ else
 | 
				
			|||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test 19: Long-term Key Management Separation
 | 
					# Test 19: Long-term Key Management Separation
 | 
				
			||||||
print_step "19" "Testing long-term key management separation"
 | 
					print_step "19" "Testing long-term key access via different unlock key types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo "Testing that unlock keys manage their own long-term keys..."
 | 
					echo "Testing that different unlock key types can access the same long-term key..."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Switch between different unlock methods to verify each handles its own long-term keys
 | 
					# Switch between different unlock methods to verify each can access the long-term key
 | 
				
			||||||
echo "Testing mnemonic-based long-term key management..."
 | 
					echo "Testing mnemonic-based long-term key access..."
 | 
				
			||||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
					export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
				
			||||||
if echo "mnemonic-longterm-test" | $SECRET_BINARY add "longterm/mnemonic" > /dev/null 2>&1; then
 | 
					echo "Running: echo \"mnemonic-longterm-test\" | $SECRET_BINARY add \"longterm/mnemonic\""
 | 
				
			||||||
    if $SECRET_BINARY get "longterm/mnemonic" > /dev/null 2>&1; then
 | 
					if echo "mnemonic-longterm-test" | $SECRET_BINARY add "longterm/mnemonic"; then
 | 
				
			||||||
        print_success "Mnemonic-based long-term key management working"
 | 
					    echo "Running: $SECRET_BINARY get \"longterm/mnemonic\""
 | 
				
			||||||
 | 
					    if $SECRET_BINARY get "longterm/mnemonic"; then
 | 
				
			||||||
 | 
					        print_success "Mnemonic-based long-term key access working"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_error "Mnemonic-based long-term key management failed"
 | 
					        print_error "Mnemonic-based long-term key access failed"
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to test mnemonic-based long-term key management"
 | 
					    print_error "Failed to test mnemonic-based long-term key access"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo "Testing passphrase-based long-term key management..."
 | 
					echo "Testing passphrase unlock key accessing long-term key..."
 | 
				
			||||||
unset SB_SECRET_MNEMONIC
 | 
					unset SB_SECRET_MNEMONIC
 | 
				
			||||||
if echo "passphrase-longterm-test" | $SECRET_BINARY add "longterm/passphrase" > /dev/null 2>&1; then
 | 
					echo "Running: echo \"passphrase-unlock-test\" | $SECRET_BINARY add \"longterm/passphrase-unlock\""
 | 
				
			||||||
    if $SECRET_BINARY get "longterm/passphrase" > /dev/null 2>&1; then
 | 
					if echo "passphrase-unlock-test" | $SECRET_BINARY add "longterm/passphrase-unlock"; then
 | 
				
			||||||
        print_success "Passphrase-based long-term key management working"
 | 
					    echo "Running: echo \"$TEST_PASSPHRASE\" | $SECRET_BINARY get \"longterm/passphrase-unlock\""
 | 
				
			||||||
 | 
					    if echo "$TEST_PASSPHRASE" | $SECRET_BINARY get "longterm/passphrase-unlock"; then
 | 
				
			||||||
 | 
					        print_success "Passphrase unlock key accessing long-term key working"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_error "Passphrase-based long-term key management failed"
 | 
					        print_error "Passphrase unlock key accessing long-term key failed"
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_error "Failed to test passphrase-based long-term key management"
 | 
					    print_error "Failed to test passphrase unlock key accessing long-term key"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Re-enable mnemonic
 | 
					# Re-enable mnemonic for remaining tests
 | 
				
			||||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
					export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test 20: Directory Structure and File Access Patterns
 | 
					# Test 20: Directory Structure and File Access Patterns
 | 
				
			||||||
@ -826,7 +894,8 @@ echo "Verifying secrets access their own directory structure..."
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Check that secret directories contain the expected structure
 | 
					# Check that secret directories contain the expected structure
 | 
				
			||||||
SECRET_NAME="structure/test"
 | 
					SECRET_NAME="structure/test"
 | 
				
			||||||
if echo "structure-test-value" | $SECRET_BINARY add "$SECRET_NAME" > /dev/null 2>&1; then
 | 
					echo "Running: echo \"structure-test-value\" | $SECRET_BINARY add $SECRET_NAME"
 | 
				
			||||||
 | 
					if echo "structure-test-value" | $SECRET_BINARY add "$SECRET_NAME"; then
 | 
				
			||||||
    print_success "Created secret for structure testing"
 | 
					    print_success "Created secret for structure testing"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Convert secret name to directory name (URL encoding)
 | 
					    # Convert secret name to directory name (URL encoding)
 | 
				
			||||||
@ -837,7 +906,8 @@ if echo "structure-test-value" | $SECRET_BINARY add "$SECRET_NAME" > /dev/null 2
 | 
				
			|||||||
        print_success "Secret directory structure created correctly"
 | 
					        print_success "Secret directory structure created correctly"
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Verify secret can access its own encrypted data
 | 
					        # Verify secret can access its own encrypted data
 | 
				
			||||||
        if $SECRET_BINARY get "$SECRET_NAME" > /dev/null 2>&1; then
 | 
					        echo "Running: $SECRET_BINARY get $SECRET_NAME"
 | 
				
			||||||
 | 
					        if $SECRET_BINARY get "$SECRET_NAME"; then
 | 
				
			||||||
            print_success "Secret correctly accesses its own encrypted data"
 | 
					            print_success "Secret correctly accesses its own encrypted data"
 | 
				
			||||||
        else
 | 
					        else
 | 
				
			||||||
            print_error "Secret failed to access its own encrypted data"
 | 
					            print_error "Secret failed to access its own encrypted data"
 | 
				
			||||||
@ -886,7 +956,8 @@ print_step "21" "Testing error handling in refactored architecture"
 | 
				
			|||||||
echo "Testing secret error handling..."
 | 
					echo "Testing secret error handling..."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Test non-existent secret
 | 
					# Test non-existent secret
 | 
				
			||||||
if $SECRET_BINARY get "nonexistent/secret" > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY get \"nonexistent/secret\""
 | 
				
			||||||
 | 
					if $SECRET_BINARY get "nonexistent/secret"; then
 | 
				
			||||||
    print_error "Should have failed for non-existent secret"
 | 
					    print_error "Should have failed for non-existent secret"
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    print_success "Correctly handled non-existent secret"
 | 
					    print_success "Correctly handled non-existent secret"
 | 
				
			||||||
@ -904,7 +975,8 @@ if [ -d "$FIRST_KEY_DIR" ] && [ -f "$FIRST_KEY_DIR/priv.age" ]; then
 | 
				
			|||||||
    # Temporarily disable mnemonic to force unlock key usage
 | 
					    # Temporarily disable mnemonic to force unlock key usage
 | 
				
			||||||
    unset SB_SECRET_MNEMONIC
 | 
					    unset SB_SECRET_MNEMONIC
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if $SECRET_BINARY get "database/password" > /dev/null 2>&1; then
 | 
					    echo "Running: $SECRET_BINARY get \"database/password\""
 | 
				
			||||||
 | 
					    if $SECRET_BINARY get "database/password"; then
 | 
				
			||||||
        print_warning "Expected failure with corrupted unlock key, but succeeded (may have fallback)"
 | 
					        print_warning "Expected failure with corrupted unlock key, but succeeded (may have fallback)"
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
        print_success "Correctly handled corrupted unlock key"
 | 
					        print_success "Correctly handled corrupted unlock key"
 | 
				
			||||||
@ -925,27 +997,33 @@ print_step "22" "Testing cross-component integration"
 | 
				
			|||||||
echo "Testing vault-secret-unlock key integration..."
 | 
					echo "Testing vault-secret-unlock key integration..."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Create a secret in one vault, switch vaults, create another secret, switch back
 | 
					# Create a secret in one vault, switch vaults, create another secret, switch back
 | 
				
			||||||
if $SECRET_BINARY vault create integration-test > /dev/null 2>&1; then
 | 
					echo "Running: $SECRET_BINARY vault create integration-test"
 | 
				
			||||||
 | 
					if $SECRET_BINARY vault create integration-test; then
 | 
				
			||||||
    print_success "Created integration test vault"
 | 
					    print_success "Created integration test vault"
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Add secret to default vault
 | 
					    # Add secret to default vault
 | 
				
			||||||
    if echo "default-vault-secret" | $SECRET_BINARY add "integration/default" > /dev/null 2>&1; then
 | 
					    echo "Running: echo \"default-vault-secret\" | $SECRET_BINARY add \"integration/default\""
 | 
				
			||||||
 | 
					    if echo "default-vault-secret" | $SECRET_BINARY add "integration/default"; then
 | 
				
			||||||
        print_success "Added secret to default vault"
 | 
					        print_success "Added secret to default vault"
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        # Switch to integration-test vault
 | 
					        # Switch to integration-test vault
 | 
				
			||||||
        if $SECRET_BINARY vault select integration-test > /dev/null 2>&1; then
 | 
					        echo "Running: $SECRET_BINARY vault select integration-test"
 | 
				
			||||||
 | 
					        if $SECRET_BINARY vault select integration-test; then
 | 
				
			||||||
            print_success "Switched to integration-test vault"
 | 
					            print_success "Switched to integration-test vault"
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            # Create unlock key in new vault
 | 
					            # Create unlock key in new vault
 | 
				
			||||||
            if echo "integration-passphrase" | $SECRET_BINARY keys add passphrase > /dev/null 2>&1; then
 | 
					            echo "Running: echo \"integration-passphrase\" | $SECRET_BINARY keys add passphrase"
 | 
				
			||||||
 | 
					            if echo "integration-passphrase" | $SECRET_BINARY keys add passphrase; then
 | 
				
			||||||
                print_success "Created unlock key in integration-test vault"
 | 
					                print_success "Created unlock key in integration-test vault"
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                # Add secret to integration-test vault
 | 
					                # Add secret to integration-test vault
 | 
				
			||||||
                if echo "integration-vault-secret" | $SECRET_BINARY add "integration/test" > /dev/null 2>&1; then
 | 
					                echo "Running: echo \"integration-vault-secret\" | $SECRET_BINARY add \"integration/test\""
 | 
				
			||||||
 | 
					                if echo "integration-vault-secret" | $SECRET_BINARY add "integration/test"; then
 | 
				
			||||||
                    print_success "Added secret to integration-test vault"
 | 
					                    print_success "Added secret to integration-test vault"
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                    # Verify secret retrieval works
 | 
					                    # Verify secret retrieval works
 | 
				
			||||||
                    if $SECRET_BINARY get "integration/test" > /dev/null 2>&1; then
 | 
					                    echo "Running: $SECRET_BINARY get \"integration/test\""
 | 
				
			||||||
 | 
					                    if $SECRET_BINARY get "integration/test"; then
 | 
				
			||||||
                        print_success "Cross-component integration working"
 | 
					                        print_success "Cross-component integration working"
 | 
				
			||||||
                    else
 | 
					                    else
 | 
				
			||||||
                        print_error "Cross-component integration failed"
 | 
					                        print_error "Cross-component integration failed"
 | 
				
			||||||
@ -958,11 +1036,13 @@ if $SECRET_BINARY vault create integration-test > /dev/null 2>&1; then
 | 
				
			|||||||
            fi
 | 
					            fi
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            # Switch back to default vault
 | 
					            # Switch back to default vault
 | 
				
			||||||
            if $SECRET_BINARY vault select default > /dev/null 2>&1; then
 | 
					            echo "Running: $SECRET_BINARY vault select default"
 | 
				
			||||||
 | 
					            if $SECRET_BINARY vault select default; then
 | 
				
			||||||
                print_success "Switched back to default vault"
 | 
					                print_success "Switched back to default vault"
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                # Verify we can still access default vault secrets
 | 
					                # Verify we can still access default vault secrets
 | 
				
			||||||
                if $SECRET_BINARY get "integration/default" > /dev/null 2>&1; then
 | 
					                echo "Running: $SECRET_BINARY get \"integration/default\""
 | 
				
			||||||
 | 
					                if $SECRET_BINARY get "integration/default"; then
 | 
				
			||||||
                    print_success "Can still access default vault secrets"
 | 
					                    print_success "Can still access default vault secrets"
 | 
				
			||||||
                else
 | 
					                else
 | 
				
			||||||
                    print_error "Cannot access default vault secrets after switching"
 | 
					                    print_error "Cannot access default vault secrets after switching"
 | 
				
			||||||
@ -999,7 +1079,7 @@ echo -e "${GREEN}✓ Mixed approach compatibility${NC}"
 | 
				
			|||||||
echo -e "${GREEN}✓ Error handling${NC}"
 | 
					echo -e "${GREEN}✓ Error handling${NC}"
 | 
				
			||||||
echo -e "${GREEN}✓ Refactored architecture - separation of concerns${NC}"
 | 
					echo -e "${GREEN}✓ Refactored architecture - separation of concerns${NC}"
 | 
				
			||||||
echo -e "${GREEN}✓ Interface method compliance${NC}"
 | 
					echo -e "${GREEN}✓ Interface method compliance${NC}"
 | 
				
			||||||
echo -e "${GREEN}✓ Long-term key management separation${NC}"
 | 
					echo -e "${GREEN}✓ Long-term key access via different unlock key types${NC}"
 | 
				
			||||||
echo -e "${GREEN}✓ Directory structure and file access patterns${NC}"
 | 
					echo -e "${GREEN}✓ Directory structure and file access patterns${NC}"
 | 
				
			||||||
echo -e "${GREEN}✓ Error handling in refactored architecture${NC}"
 | 
					echo -e "${GREEN}✓ Error handling in refactored architecture${NC}"
 | 
				
			||||||
echo -e "${GREEN}✓ Cross-component integration${NC}"
 | 
					echo -e "${GREEN}✓ Cross-component integration${NC}"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user