Compare commits
	
		
			3 Commits
		
	
	
		
			8cc15fde3d
			...
			85d7ef21eb
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 85d7ef21eb | |||
| a4d7225036 | |||
| 8dc2e9d748 | 
@ -6,7 +6,6 @@ import (
 | 
				
			|||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"path/filepath"
 | 
						"path/filepath"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"syscall"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"filippo.io/age"
 | 
						"filippo.io/age"
 | 
				
			||||||
	"git.eeqj.de/sneak/secret/internal/secret"
 | 
						"git.eeqj.de/sneak/secret/internal/secret"
 | 
				
			||||||
@ -15,7 +14,6 @@ import (
 | 
				
			|||||||
	"github.com/spf13/afero"
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
	"github.com/tyler-smith/go-bip39"
 | 
						"github.com/tyler-smith/go-bip39"
 | 
				
			||||||
	"golang.org/x/term"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newInitCmd() *cobra.Command {
 | 
					func newInitCmd() *cobra.Command {
 | 
				
			||||||
@ -38,7 +36,7 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
 | 
				
			|||||||
	stateDir := cli.GetStateDir()
 | 
						stateDir := cli.GetStateDir()
 | 
				
			||||||
	secret.DebugWith("Creating state directory", slog.String("path", stateDir))
 | 
						secret.DebugWith("Creating state directory", slog.String("path", stateDir))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := cli.fs.MkdirAll(stateDir, 0700); err != nil {
 | 
						if err := cli.fs.MkdirAll(stateDir, secret.DirPerms); err != nil {
 | 
				
			||||||
		secret.Debug("Failed to create state directory", "error", err)
 | 
							secret.Debug("Failed to create state directory", "error", err)
 | 
				
			||||||
		return fmt.Errorf("failed to create state directory: %w", err)
 | 
							return fmt.Errorf("failed to create state directory: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -103,7 +101,7 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
 | 
				
			|||||||
	vaultDir := filepath.Join(stateDir, "vaults.d", "default")
 | 
						vaultDir := filepath.Join(stateDir, "vaults.d", "default")
 | 
				
			||||||
	ltPubKey := ltIdentity.Recipient().String()
 | 
						ltPubKey := ltIdentity.Recipient().String()
 | 
				
			||||||
	secret.DebugWith("Storing long-term public key", slog.String("pubkey", ltPubKey), slog.String("vault_dir", vaultDir))
 | 
						secret.DebugWith("Storing long-term public key", slog.String("pubkey", ltPubKey), slog.String("vault_dir", vaultDir))
 | 
				
			||||||
	if err := afero.WriteFile(cli.fs, filepath.Join(vaultDir, "pub.age"), []byte(ltPubKey), 0600); err != nil {
 | 
						if err := afero.WriteFile(cli.fs, filepath.Join(vaultDir, "pub.age"), []byte(ltPubKey), secret.FilePerms); err != nil {
 | 
				
			||||||
		secret.Debug("Failed to write long-term public key", "error", err)
 | 
							secret.Debug("Failed to write long-term public key", "error", err)
 | 
				
			||||||
		return fmt.Errorf("failed to write long-term public key: %w", err)
 | 
							return fmt.Errorf("failed to write long-term public key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -156,7 +154,7 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Write encrypted long-term private key
 | 
						// Write encrypted long-term private key
 | 
				
			||||||
	if err := afero.WriteFile(cli.fs, filepath.Join(unlockKeyDir, "longterm.age"), encryptedLtPrivKey, 0600); err != nil {
 | 
						if err := afero.WriteFile(cli.fs, filepath.Join(unlockKeyDir, "longterm.age"), encryptedLtPrivKey, secret.FilePerms); err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
							return fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -173,45 +171,24 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 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.
 | 
					// This version adds confirmation (read twice) for creating new unlock keys
 | 
				
			||||||
func readSecurePassphrase(prompt string) (string, error) {
 | 
					func readSecurePassphrase(prompt string) (string, error) {
 | 
				
			||||||
	// Check if stdin is a terminal
 | 
						// Get the first passphrase
 | 
				
			||||||
	if !term.IsTerminal(int(syscall.Stdin)) {
 | 
						passphrase1, err := secret.ReadPassphrase(prompt)
 | 
				
			||||||
		// Not a terminal - never read passphrases from piped input for security reasons
 | 
					 | 
				
			||||||
		return "", fmt.Errorf("cannot read passphrase from non-terminal stdin (piped input or script). Please set the SB_UNLOCK_PASSPHRASE environment variable or run interactively")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 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). Please set the SB_UNLOCK_PASSPHRASE environment variable")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Terminal input - use secure password reading with confirmation
 | 
					 | 
				
			||||||
	fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Read first passphrase
 | 
					 | 
				
			||||||
	passphrase1, err := term.ReadPassword(int(syscall.Stdin))
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", fmt.Errorf("failed to read passphrase: %w", err)
 | 
							return "", err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fmt.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Read confirmation passphrase
 | 
						// Read confirmation passphrase
 | 
				
			||||||
	fmt.Fprint(os.Stderr, "Confirm passphrase: ") // Write prompt to stderr, not stdout
 | 
						passphrase2, err := secret.ReadPassphrase("Confirm passphrase: ")
 | 
				
			||||||
	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.Fprintln(os.Stderr) // Print newline to stderr since ReadPassword doesn't echo
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Compare passphrases
 | 
						// Compare passphrases
 | 
				
			||||||
	if string(passphrase1) != string(passphrase2) {
 | 
						if passphrase1 != passphrase2 {
 | 
				
			||||||
		return "", fmt.Errorf("passphrases do not match")
 | 
							return "", fmt.Errorf("passphrases do not match")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(passphrase1) == 0 {
 | 
						return passphrase1, nil
 | 
				
			||||||
		return "", fmt.Errorf("passphrase cannot be empty")
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return string(passphrase1), nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -14,6 +14,10 @@ import (
 | 
				
			|||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Import from init.go
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ... existing imports ...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newKeysCmd() *cobra.Command {
 | 
					func newKeysCmd() *cobra.Command {
 | 
				
			||||||
	cmd := &cobra.Command{
 | 
						cmd := &cobra.Command{
 | 
				
			||||||
		Use:   "keys",
 | 
							Use:   "keys",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,7 @@
 | 
				
			|||||||
package secret
 | 
					package secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "os"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	// AppID is the unique identifier for this application
 | 
						// AppID is the unique identifier for this application
 | 
				
			||||||
	AppID = "berlin.sneak.pkg.secret"
 | 
						AppID = "berlin.sneak.pkg.secret"
 | 
				
			||||||
@ -10,3 +12,12 @@ const (
 | 
				
			|||||||
	EnvUnlockPassphrase = "SB_UNLOCK_PASSPHRASE"
 | 
						EnvUnlockPassphrase = "SB_UNLOCK_PASSPHRASE"
 | 
				
			||||||
	EnvGPGKeyID         = "SB_GPG_KEY_ID"
 | 
						EnvGPGKeyID         = "SB_GPG_KEY_ID"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// File system permission constants
 | 
				
			||||||
 | 
					const (
 | 
				
			||||||
 | 
						// DirPerms is the permission used for directories (read-write-execute for owner only)
 | 
				
			||||||
 | 
						DirPerms os.FileMode = 0700
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// FilePerms is the permission used for sensitive files (read-write for owner only)
 | 
				
			||||||
 | 
						FilePerms os.FileMode = 0600
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ 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))
 | 
						Debug("EncryptToRecipient starting", "data_length", len(data))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var buf bytes.Buffer
 | 
						var buf bytes.Buffer
 | 
				
			||||||
	Debug("Creating age encryptor")
 | 
						Debug("Creating age encryptor")
 | 
				
			||||||
@ -39,22 +39,12 @@ func EncryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
 | 
				
			|||||||
	Debug("Closed encryptor successfully")
 | 
						Debug("Closed encryptor successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	result := buf.Bytes()
 | 
						result := buf.Bytes()
 | 
				
			||||||
	Debug("encryptToRecipient completed successfully", "result_length", len(result))
 | 
						Debug("EncryptToRecipient completed successfully", "result_length", len(result))
 | 
				
			||||||
	return result, nil
 | 
						return result, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// encryptToRecipient encrypts data to a recipient using age (internal version)
 | 
					// DecryptWithIdentity decrypts data with an identity using age
 | 
				
			||||||
func encryptToRecipient(data []byte, recipient age.Recipient) ([]byte, error) {
 | 
					 | 
				
			||||||
	return EncryptToRecipient(data, recipient)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// DecryptWithIdentity decrypts data with an identity using age (public version)
 | 
					 | 
				
			||||||
func DecryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) {
 | 
					func DecryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) {
 | 
				
			||||||
	return decryptWithIdentity(data, identity)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// decryptWithIdentity decrypts data with an identity using age
 | 
					 | 
				
			||||||
func decryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) {
 | 
					 | 
				
			||||||
	r, err := age.Decrypt(bytes.NewReader(data), identity)
 | 
						r, err := age.Decrypt(bytes.NewReader(data), identity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create decryptor: %w", err)
 | 
							return nil, fmt.Errorf("failed to create decryptor: %w", err)
 | 
				
			||||||
@ -68,34 +58,29 @@ func decryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) {
 | 
				
			|||||||
	return result, nil
 | 
						return result, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// EncryptWithPassphrase encrypts data using a passphrase with age's scrypt-based encryption (public version)
 | 
					// EncryptWithPassphrase encrypts data using a passphrase with age's scrypt-based encryption
 | 
				
			||||||
func EncryptWithPassphrase(data []byte, passphrase string) ([]byte, error) {
 | 
					func EncryptWithPassphrase(data []byte, passphrase string) ([]byte, error) {
 | 
				
			||||||
	return encryptWithPassphrase(data, passphrase)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// encryptWithPassphrase encrypts data using a passphrase with age's scrypt-based encryption
 | 
					 | 
				
			||||||
func encryptWithPassphrase(data []byte, passphrase string) ([]byte, error) {
 | 
					 | 
				
			||||||
	recipient, err := age.NewScryptRecipient(passphrase)
 | 
						recipient, err := age.NewScryptRecipient(passphrase)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
 | 
							return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return encryptToRecipient(data, recipient)
 | 
						return EncryptToRecipient(data, recipient)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// decryptWithPassphrase decrypts data using a passphrase with age's scrypt-based decryption
 | 
					// DecryptWithPassphrase decrypts data using a passphrase with age's scrypt-based decryption
 | 
				
			||||||
func decryptWithPassphrase(encryptedData []byte, passphrase string) ([]byte, error) {
 | 
					func DecryptWithPassphrase(encryptedData []byte, passphrase string) ([]byte, error) {
 | 
				
			||||||
	identity, err := age.NewScryptIdentity(passphrase)
 | 
						identity, err := age.NewScryptIdentity(passphrase)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create scrypt identity: %w", err)
 | 
							return nil, fmt.Errorf("failed to create scrypt identity: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return decryptWithIdentity(encryptedData, identity)
 | 
						return DecryptWithIdentity(encryptedData, identity)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 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 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 - never read passphrases from piped input for security reasons
 | 
							// Not a terminal - never read passphrases from piped input for security reasons
 | 
				
			||||||
 | 
				
			|||||||
@ -91,7 +91,7 @@ func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Step 5: Decrypt the age private key using the passphrase from keychain
 | 
						// Step 5: Decrypt the age private key using the passphrase from keychain
 | 
				
			||||||
	Debug("Decrypting age private key with keychain passphrase", "key_id", k.GetID())
 | 
						Debug("Decrypting age private key with keychain passphrase", "key_id", k.GetID())
 | 
				
			||||||
	agePrivKeyData, err := decryptWithPassphrase(encryptedAgePrivKeyData, keychainData.AgePrivKeyPassphrase)
 | 
						agePrivKeyData, err := DecryptWithPassphrase(encryptedAgePrivKeyData, keychainData.AgePrivKeyPassphrase)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt age private key with keychain passphrase", "error", err, "key_id", k.GetID())
 | 
							Debug("Failed to decrypt age private key with keychain passphrase", "error", err, "key_id", k.GetID())
 | 
				
			||||||
		return nil, fmt.Errorf("failed to decrypt age private key with keychain passphrase: %w", err)
 | 
							return nil, fmt.Errorf("failed to decrypt age private key with keychain passphrase: %w", err)
 | 
				
			||||||
@ -240,7 +240,7 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unlockKeyDir := filepath.Join(vaultDir, "unlock.d", keychainItemName)
 | 
						unlockKeyDir := filepath.Join(vaultDir, "unlock.d", keychainItemName)
 | 
				
			||||||
	if err := fs.MkdirAll(unlockKeyDir, 0700); err != nil {
 | 
						if err := fs.MkdirAll(unlockKeyDir, DirPerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
 | 
							return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -259,19 +259,19 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
 | 
				
			|||||||
	// Step 3: Store age public key as plaintext
 | 
						// Step 3: Store age public key as plaintext
 | 
				
			||||||
	agePublicKeyString := ageIdentity.Recipient().String()
 | 
						agePublicKeyString := ageIdentity.Recipient().String()
 | 
				
			||||||
	agePubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
 | 
						agePubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
 | 
				
			||||||
	if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), 0600); err != nil {
 | 
						if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write age public key: %w", err)
 | 
							return nil, fmt.Errorf("failed to write age public key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Step 4: Encrypt age private key with the generated passphrase and store on disk
 | 
						// Step 4: Encrypt age private key with the generated passphrase and store on disk
 | 
				
			||||||
	agePrivateKeyBytes := []byte(ageIdentity.String())
 | 
						agePrivateKeyBytes := []byte(ageIdentity.String())
 | 
				
			||||||
	encryptedAgePrivKey, err := encryptWithPassphrase(agePrivateKeyBytes, agePrivKeyPassphrase)
 | 
						encryptedAgePrivKey, err := EncryptWithPassphrase(agePrivateKeyBytes, agePrivKeyPassphrase)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to encrypt age private key with passphrase: %w", err)
 | 
							return nil, fmt.Errorf("failed to encrypt age private key with passphrase: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age")
 | 
						agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age")
 | 
				
			||||||
	if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, 0600); err != nil {
 | 
						if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -328,21 +328,21 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Decrypt long-term private key using current unlock key
 | 
							// Decrypt long-term private key using current unlock key
 | 
				
			||||||
		ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
 | 
							ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
								return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Step 6: Encrypt long-term private key to the new age unlock key
 | 
						// Step 6: Encrypt long-term private key to the new age unlock key
 | 
				
			||||||
	encryptedLtPrivKeyToAge, err := encryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
 | 
						encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to encrypt long-term private key to age unlock key: %w", err)
 | 
							return nil, fmt.Errorf("failed to encrypt long-term private key to age unlock key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Write encrypted long-term private key
 | 
						// Write encrypted long-term private key
 | 
				
			||||||
	ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
 | 
						ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
 | 
				
			||||||
	if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, 0600); err != nil {
 | 
						if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -383,7 +383,7 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
 | 
				
			|||||||
		return nil, fmt.Errorf("failed to marshal unlock key metadata: %w", err)
 | 
							return nil, fmt.Errorf("failed to marshal unlock key metadata: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := afero.WriteFile(fs, filepath.Join(unlockKeyDir, "unlock-metadata.json"), metadataBytes, 0600); err != nil {
 | 
						if err := afero.WriteFile(fs, filepath.Join(unlockKeyDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
 | 
							return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										193
									
								
								internal/secret/passphrase_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								internal/secret/passphrase_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,193 @@
 | 
				
			|||||||
 | 
					package secret_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"filippo.io/age"
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/internal/secret"
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/pkg/agehd"
 | 
				
			||||||
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
 | 
				
			||||||
 | 
						// Skip this test if CI=true is set, as it uses real filesystem
 | 
				
			||||||
 | 
						if os.Getenv("CI") == "true" {
 | 
				
			||||||
 | 
							t.Skip("Skipping test with real filesystem in CI environment")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create a temporary directory for our tests
 | 
				
			||||||
 | 
						tempDir, err := os.MkdirTemp("", "secret-passphrase-test-")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create temp dir: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer os.RemoveAll(tempDir) // Clean up after test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use the real filesystem
 | 
				
			||||||
 | 
						fs := afero.NewOsFs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test data
 | 
				
			||||||
 | 
						testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
 | 
				
			||||||
 | 
						testPassphrase := "test-passphrase-123"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create the directory structure
 | 
				
			||||||
 | 
						keyDir := filepath.Join(tempDir, "unlock-key")
 | 
				
			||||||
 | 
						if err := os.MkdirAll(keyDir, secret.DirPerms); err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create key directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set up test metadata
 | 
				
			||||||
 | 
						metadata := secret.UnlockKeyMetadata{
 | 
				
			||||||
 | 
							ID:        "test-passphrase",
 | 
				
			||||||
 | 
							Type:      "passphrase",
 | 
				
			||||||
 | 
							CreatedAt: time.Now(),
 | 
				
			||||||
 | 
							Flags:     []string{},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create passphrase unlock key
 | 
				
			||||||
 | 
						unlockKey := secret.NewPassphraseUnlockKey(fs, keyDir, metadata)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate a test age identity
 | 
				
			||||||
 | 
						ageIdentity, err := age.GenerateX25519Identity()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to generate age identity: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						agePrivateKey := ageIdentity.String()
 | 
				
			||||||
 | 
						agePublicKey := ageIdentity.Recipient().String()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test writing public key
 | 
				
			||||||
 | 
						t.Run("WritePublicKey", func(t *testing.T) {
 | 
				
			||||||
 | 
							pubKeyPath := filepath.Join(keyDir, "pub.age")
 | 
				
			||||||
 | 
							if err := afero.WriteFile(fs, pubKeyPath, []byte(agePublicKey), secret.FilePerms); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to write public key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the file exists
 | 
				
			||||||
 | 
							exists, err := afero.Exists(fs, pubKeyPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to check if public key exists: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !exists {
 | 
				
			||||||
 | 
								t.Errorf("Public key file should exist at %s", pubKeyPath)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test encrypting private key with passphrase
 | 
				
			||||||
 | 
						t.Run("EncryptPrivateKey", func(t *testing.T) {
 | 
				
			||||||
 | 
							privKeyData := []byte(agePrivateKey)
 | 
				
			||||||
 | 
							encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyData, testPassphrase)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to encrypt private key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							privKeyPath := filepath.Join(keyDir, "priv.age")
 | 
				
			||||||
 | 
							if err := afero.WriteFile(fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to write encrypted private key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the file exists
 | 
				
			||||||
 | 
							exists, err := afero.Exists(fs, privKeyPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to check if private key exists: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !exists {
 | 
				
			||||||
 | 
								t.Errorf("Encrypted private key file should exist at %s", privKeyPath)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test writing long-term key
 | 
				
			||||||
 | 
						t.Run("WriteLongTermKey", func(t *testing.T) {
 | 
				
			||||||
 | 
							// Derive a long-term identity from the test mnemonic
 | 
				
			||||||
 | 
							ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to derive long-term identity: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Encrypt long-term private key to the unlock key's recipient
 | 
				
			||||||
 | 
							recipient, err := age.ParseX25519Recipient(agePublicKey)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to parse recipient: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ltPrivKeyData := []byte(ltIdentity.String())
 | 
				
			||||||
 | 
							encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyData, recipient)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to encrypt long-term private key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							ltPrivKeyPath := filepath.Join(keyDir, "longterm.age")
 | 
				
			||||||
 | 
							if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to write encrypted long-term private key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the file exists
 | 
				
			||||||
 | 
							exists, err := afero.Exists(fs, ltPrivKeyPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to check if long-term key exists: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !exists {
 | 
				
			||||||
 | 
								t.Errorf("Encrypted long-term key file should exist at %s", ltPrivKeyPath)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Save original environment variables and set test ones
 | 
				
			||||||
 | 
						oldPassphrase := os.Getenv(secret.EnvUnlockPassphrase)
 | 
				
			||||||
 | 
						os.Setenv(secret.EnvUnlockPassphrase, testPassphrase)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Clean up after test
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if oldPassphrase != "" {
 | 
				
			||||||
 | 
								os.Setenv(secret.EnvUnlockPassphrase, oldPassphrase)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Unsetenv(secret.EnvUnlockPassphrase)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting identity from environment variable
 | 
				
			||||||
 | 
						t.Run("GetIdentityFromEnv", func(t *testing.T) {
 | 
				
			||||||
 | 
							identity, err := unlockKey.GetIdentity()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to get identity from env: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the identity matches what we expect
 | 
				
			||||||
 | 
							expectedPubKey := ageIdentity.Recipient().String()
 | 
				
			||||||
 | 
							actualPubKey := identity.Recipient().String()
 | 
				
			||||||
 | 
							if actualPubKey != expectedPubKey {
 | 
				
			||||||
 | 
								t.Errorf("Public key mismatch. Expected %s, got %s", expectedPubKey, actualPubKey)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Unset the environment variable to test interactive prompt
 | 
				
			||||||
 | 
						os.Unsetenv(secret.EnvUnlockPassphrase)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting identity from prompt (this would require mocking the prompt)
 | 
				
			||||||
 | 
						// For real integration tests, we'd need to provide a way to mock the passphrase input
 | 
				
			||||||
 | 
						// Here we'll just verify the error is what we expect when no passphrase is available
 | 
				
			||||||
 | 
						t.Run("GetIdentityWithoutEnv", func(t *testing.T) {
 | 
				
			||||||
 | 
							// This should fail since we're not in an interactive terminal
 | 
				
			||||||
 | 
							_, err := unlockKey.GetIdentity()
 | 
				
			||||||
 | 
							if err == nil {
 | 
				
			||||||
 | 
								t.Errorf("Should have failed to get identity without passphrase env var")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test removing the unlock key
 | 
				
			||||||
 | 
						t.Run("RemoveUnlockKey", func(t *testing.T) {
 | 
				
			||||||
 | 
							err := unlockKey.Remove()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to remove unlock key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the directory is gone
 | 
				
			||||||
 | 
							exists, err := afero.DirExists(fs, keyDir)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to check if key directory exists: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if exists {
 | 
				
			||||||
 | 
								t.Errorf("Key directory should not exist after removal")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -15,6 +15,7 @@ type PassphraseUnlockKey struct {
 | 
				
			|||||||
	Directory  string
 | 
						Directory  string
 | 
				
			||||||
	Metadata   UnlockKeyMetadata
 | 
						Metadata   UnlockKeyMetadata
 | 
				
			||||||
	fs         afero.Fs
 | 
						fs         afero.Fs
 | 
				
			||||||
 | 
						Passphrase string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetIdentity implements UnlockKey interface for passphrase-based unlock keys
 | 
					// GetIdentity implements UnlockKey interface for passphrase-based unlock keys
 | 
				
			||||||
@ -24,6 +25,28 @@ func (p *PassphraseUnlockKey) GetIdentity() (*age.X25519Identity, error) {
 | 
				
			|||||||
		slog.String("key_type", p.GetType()),
 | 
							slog.String("key_type", p.GetType()),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// First check if we already have the passphrase
 | 
				
			||||||
 | 
						passphraseStr := p.Passphrase
 | 
				
			||||||
 | 
						if passphraseStr == "" {
 | 
				
			||||||
 | 
							Debug("No passphrase in memory, checking environment")
 | 
				
			||||||
 | 
							// Check environment variable for passphrase
 | 
				
			||||||
 | 
							passphraseStr = os.Getenv(EnvUnlockPassphrase)
 | 
				
			||||||
 | 
							if passphraseStr == "" {
 | 
				
			||||||
 | 
								Debug("No passphrase in environment, prompting user")
 | 
				
			||||||
 | 
								// Prompt for passphrase
 | 
				
			||||||
 | 
								var err error
 | 
				
			||||||
 | 
								passphraseStr, err = ReadPassphrase("Enter unlock passphrase: ")
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									Debug("Failed to read passphrase", "error", err, "key_id", p.GetID())
 | 
				
			||||||
 | 
									return nil, fmt.Errorf("failed to read passphrase: %w", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								Debug("Using passphrase from environment", "key_id", p.GetID())
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							Debug("Using in-memory passphrase", "key_id", p.GetID())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Read encrypted private key of unlock key
 | 
						// Read encrypted private key of unlock key
 | 
				
			||||||
	unlockKeyPrivPath := filepath.Join(p.Directory, "priv.age")
 | 
						unlockKeyPrivPath := filepath.Join(p.Directory, "priv.age")
 | 
				
			||||||
	Debug("Reading encrypted passphrase unlock key", "path", unlockKeyPrivPath)
 | 
						Debug("Reading encrypted passphrase unlock key", "path", unlockKeyPrivPath)
 | 
				
			||||||
@ -39,25 +62,10 @@ func (p *PassphraseUnlockKey) GetIdentity() (*age.X25519Identity, error) {
 | 
				
			|||||||
		slog.Int("encrypted_length", len(encryptedPrivKeyData)),
 | 
							slog.Int("encrypted_length", len(encryptedPrivKeyData)),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get passphrase for decrypting the unlock key
 | 
					 | 
				
			||||||
	var passphraseStr string
 | 
					 | 
				
			||||||
	if envPassphrase := os.Getenv(EnvUnlockPassphrase); envPassphrase != "" {
 | 
					 | 
				
			||||||
		Debug("Using passphrase from environment variable", "key_id", p.GetID())
 | 
					 | 
				
			||||||
		passphraseStr = envPassphrase
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		Debug("Prompting for passphrase", "key_id", p.GetID())
 | 
					 | 
				
			||||||
		// Prompt for passphrase
 | 
					 | 
				
			||||||
		passphraseStr, err = readPassphrase("Enter passphrase to unlock vault: ")
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			Debug("Failed to read passphrase", "error", err, "key_id", p.GetID())
 | 
					 | 
				
			||||||
			return nil, fmt.Errorf("failed to read passphrase: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	Debug("Decrypting unlock key private key with passphrase", "key_id", p.GetID())
 | 
						Debug("Decrypting unlock key private key with passphrase", "key_id", p.GetID())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Decrypt the unlock key private key with passphrase
 | 
						// Decrypt the unlock key private key with passphrase
 | 
				
			||||||
	privKeyData, err := decryptWithPassphrase(encryptedPrivKeyData, passphraseStr)
 | 
						privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseStr)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt unlock key private key", "error", err, "key_id", p.GetID())
 | 
							Debug("Failed to decrypt unlock key private key", "error", err, "key_id", p.GetID())
 | 
				
			||||||
		return nil, fmt.Errorf("failed to decrypt unlock key private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to decrypt unlock key private key: %w", err)
 | 
				
			||||||
 | 
				
			|||||||
@ -188,7 +188,7 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	unlockKeyDir := filepath.Join(vaultDir, "unlock.d", unlockKeyName)
 | 
						unlockKeyDir := filepath.Join(vaultDir, "unlock.d", unlockKeyName)
 | 
				
			||||||
	if err := fs.MkdirAll(unlockKeyDir, 0700); err != nil {
 | 
						if err := fs.MkdirAll(unlockKeyDir, DirPerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
 | 
							return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -201,7 +201,7 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
 | 
				
			|||||||
	// Step 2: Store age public key as plaintext
 | 
						// Step 2: Store age public key as plaintext
 | 
				
			||||||
	agePublicKeyString := ageIdentity.Recipient().String()
 | 
						agePublicKeyString := ageIdentity.Recipient().String()
 | 
				
			||||||
	agePubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
 | 
						agePubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
 | 
				
			||||||
	if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), 0600); err != nil {
 | 
						if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write age public key: %w", err)
 | 
							return nil, fmt.Errorf("failed to write age public key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -250,26 +250,26 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
 | 
				
			|||||||
			return nil, fmt.Errorf("unsupported current unlock key type for PGP unlock key creation")
 | 
								return nil, fmt.Errorf("unsupported current unlock key type for PGP unlock key creation")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Decrypt long-term private key using current unlock key
 | 
							// Step 6: Decrypt long-term private key using current unlock key
 | 
				
			||||||
		ltPrivKeyData, err = decryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
 | 
							ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
								return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Step 4: Encrypt long-term private key to the new age unlock key
 | 
						// Step 7: Encrypt long-term private key to the new age unlock key
 | 
				
			||||||
	encryptedLtPrivKeyToAge, err := encryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
 | 
						encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to encrypt long-term private key to age unlock key: %w", err)
 | 
							return nil, fmt.Errorf("failed to encrypt long-term private key to age unlock key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Write encrypted long-term private key
 | 
						// Write encrypted long-term private key
 | 
				
			||||||
	ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
 | 
						ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
 | 
				
			||||||
	if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, 0600); err != nil {
 | 
						if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Step 5: Encrypt age private key to the GPG key ID
 | 
						// Step 8: Encrypt age private key to the GPG key ID
 | 
				
			||||||
	agePrivateKeyBytes := []byte(ageIdentity.String())
 | 
						agePrivateKeyBytes := []byte(ageIdentity.String())
 | 
				
			||||||
	encryptedAgePrivKey, err := gpgEncrypt(agePrivateKeyBytes, gpgKeyID)
 | 
						encryptedAgePrivKey, err := gpgEncrypt(agePrivateKeyBytes, gpgKeyID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -277,11 +277,11 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age.gpg")
 | 
						agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age.gpg")
 | 
				
			||||||
	if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, 0600); err != nil {
 | 
						if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Step 6: Create and write enhanced metadata
 | 
						// Step 9: Create and write enhanced metadata
 | 
				
			||||||
	// Generate the key ID directly using the GPG key ID
 | 
						// Generate the key ID directly using the GPG key ID
 | 
				
			||||||
	keyID := fmt.Sprintf("%s-pgp", gpgKeyID)
 | 
						keyID := fmt.Sprintf("%s-pgp", gpgKeyID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -302,7 +302,7 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
 | 
				
			|||||||
		return nil, fmt.Errorf("failed to marshal unlock key metadata: %w", err)
 | 
							return nil, fmt.Errorf("failed to marshal unlock key metadata: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := afero.WriteFile(fs, filepath.Join(unlockKeyDir, "unlock-metadata.json"), metadataBytes, 0600); err != nil {
 | 
						if err := afero.WriteFile(fs, filepath.Join(unlockKeyDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
 | 
							return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										226
									
								
								internal/secret/pgpunlock_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								internal/secret/pgpunlock_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,226 @@
 | 
				
			|||||||
 | 
					package secret_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/exec"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/internal/secret"
 | 
				
			||||||
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestPGPUnlockKeyWithRealFS(t *testing.T) {
 | 
				
			||||||
 | 
						// Skip tests if gpg is not available
 | 
				
			||||||
 | 
						if _, err := exec.LookPath("gpg"); err != nil {
 | 
				
			||||||
 | 
							t.Skip("GPG not available, skipping PGP unlock key tests")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create a temporary directory for our tests
 | 
				
			||||||
 | 
						tempDir, err := os.MkdirTemp("", "secret-pgp-test-")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create temp dir: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer os.RemoveAll(tempDir) // Clean up after test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create a temporary GNUPGHOME
 | 
				
			||||||
 | 
						gnupgHomeDir := filepath.Join(tempDir, "gnupg")
 | 
				
			||||||
 | 
						if err := os.MkdirAll(gnupgHomeDir, 0700); err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create GNUPGHOME: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Save original GNUPGHOME
 | 
				
			||||||
 | 
						origGnupgHome := os.Getenv("GNUPGHOME")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set new GNUPGHOME
 | 
				
			||||||
 | 
						os.Setenv("GNUPGHOME", gnupgHomeDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Clean up environment after test
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if origGnupgHome != "" {
 | 
				
			||||||
 | 
								os.Setenv("GNUPGHOME", origGnupgHome)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Unsetenv("GNUPGHOME")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create GPG batch file for key generation
 | 
				
			||||||
 | 
						batchFile := filepath.Join(tempDir, "gen-key-batch")
 | 
				
			||||||
 | 
						batchContent := `%echo Generating a test key
 | 
				
			||||||
 | 
					Key-Type: RSA
 | 
				
			||||||
 | 
					Key-Length: 2048
 | 
				
			||||||
 | 
					Name-Real: Test User
 | 
				
			||||||
 | 
					Name-Email: test@example.com
 | 
				
			||||||
 | 
					Expire-Date: 0
 | 
				
			||||||
 | 
					Passphrase: test123
 | 
				
			||||||
 | 
					%commit
 | 
				
			||||||
 | 
					%echo Key generation completed
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
						if err := os.WriteFile(batchFile, []byte(batchContent), 0600); err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to write batch file: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Generate GPG key
 | 
				
			||||||
 | 
						t.Log("Generating GPG key...")
 | 
				
			||||||
 | 
						cmd := exec.Command("gpg", "--batch", "--gen-key", batchFile)
 | 
				
			||||||
 | 
						output, err := cmd.CombinedOutput()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to generate GPG key: %v\nOutput: %s", err, output)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						t.Log("GPG key generated successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get the key ID
 | 
				
			||||||
 | 
						cmd = exec.Command("gpg", "--list-secret-keys", "--with-colons")
 | 
				
			||||||
 | 
						output, err = cmd.CombinedOutput()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to list GPG keys: %v\nOutput: %s", err, output)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Parse output to get key ID
 | 
				
			||||||
 | 
						var keyID string
 | 
				
			||||||
 | 
						lines := strings.Split(string(output), "\n")
 | 
				
			||||||
 | 
						for _, line := range lines {
 | 
				
			||||||
 | 
							if strings.HasPrefix(line, "sec:") {
 | 
				
			||||||
 | 
								fields := strings.Split(line, ":")
 | 
				
			||||||
 | 
								if len(fields) >= 5 {
 | 
				
			||||||
 | 
									keyID = fields[4]
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if keyID == "" {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to find GPG key ID in output: %s", output)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						t.Logf("Generated GPG key ID: %s", keyID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Export GNUPGHOME variable to ensure subprocesses inherit it
 | 
				
			||||||
 | 
						err = os.Setenv("GNUPGHOME", gnupgHomeDir)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to set GNUPGHOME environment variable: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use the real filesystem
 | 
				
			||||||
 | 
						fs := afero.NewOsFs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test data
 | 
				
			||||||
 | 
						testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Save original environment variable
 | 
				
			||||||
 | 
						oldMnemonic := os.Getenv(secret.EnvMnemonic)
 | 
				
			||||||
 | 
						oldGPGKeyID := os.Getenv(secret.EnvGPGKeyID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set test environment variables
 | 
				
			||||||
 | 
						os.Setenv(secret.EnvMnemonic, testMnemonic)
 | 
				
			||||||
 | 
						os.Setenv(secret.EnvGPGKeyID, keyID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Clean up after test
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if oldMnemonic != "" {
 | 
				
			||||||
 | 
								os.Setenv(secret.EnvMnemonic, oldMnemonic)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Unsetenv(secret.EnvMnemonic)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if oldGPGKeyID != "" {
 | 
				
			||||||
 | 
								os.Setenv(secret.EnvGPGKeyID, oldGPGKeyID)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Unsetenv(secret.EnvGPGKeyID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create the directory structure for test
 | 
				
			||||||
 | 
						keyDir := filepath.Join(tempDir, "unlock-key")
 | 
				
			||||||
 | 
						if err := os.MkdirAll(keyDir, secret.DirPerms); err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create key directory: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set up test metadata
 | 
				
			||||||
 | 
						metadata := secret.UnlockKeyMetadata{
 | 
				
			||||||
 | 
							ID:        fmt.Sprintf("%s-pgp", keyID),
 | 
				
			||||||
 | 
							Type:      "pgp",
 | 
				
			||||||
 | 
							CreatedAt: time.Now(),
 | 
				
			||||||
 | 
							Flags:     []string{"gpg", "encrypted"},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// We'll skip the CreatePGPUnlockKey test since it requires registered vault functions
 | 
				
			||||||
 | 
						t.Run("CreatePGPUnlockKey", func(t *testing.T) {
 | 
				
			||||||
 | 
							t.Skip("Skipping test that requires registered vault functions")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create a PGP unlock key for the remaining tests
 | 
				
			||||||
 | 
						unlockKey := secret.NewPGPUnlockKey(fs, keyDir, metadata)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting GPG key ID
 | 
				
			||||||
 | 
						t.Run("GetGPGKeyID", func(t *testing.T) {
 | 
				
			||||||
 | 
							// Create PGP metadata with GPG key ID
 | 
				
			||||||
 | 
							type PGPUnlockKeyMetadata struct {
 | 
				
			||||||
 | 
								secret.UnlockKeyMetadata
 | 
				
			||||||
 | 
								GPGKeyID string `json:"gpg_key_id"`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							pgpMetadata := PGPUnlockKeyMetadata{
 | 
				
			||||||
 | 
								UnlockKeyMetadata: metadata,
 | 
				
			||||||
 | 
								GPGKeyID:          keyID,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Write metadata file
 | 
				
			||||||
 | 
							metadataPath := filepath.Join(keyDir, "unlock-metadata.json")
 | 
				
			||||||
 | 
							metadataBytes, err := json.MarshalIndent(pgpMetadata, "", "  ")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to marshal metadata: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := afero.WriteFile(fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to write metadata: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get GPG key ID
 | 
				
			||||||
 | 
							retrievedKeyID, err := unlockKey.GetGPGKeyID()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to get GPG key ID: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify key ID
 | 
				
			||||||
 | 
							if retrievedKeyID != keyID {
 | 
				
			||||||
 | 
								t.Errorf("Expected GPG key ID '%s', got '%s'", keyID, retrievedKeyID)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test getting identity from PGP unlock key
 | 
				
			||||||
 | 
						t.Run("GetIdentity", func(t *testing.T) {
 | 
				
			||||||
 | 
							// For this test, we'll do a simplified version since GPG operations
 | 
				
			||||||
 | 
							// can be tricky in automated tests
 | 
				
			||||||
 | 
							t.Skip("Skipping GetIdentity test due to complex GPG operations in automated testing")
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test removing the unlock key
 | 
				
			||||||
 | 
						t.Run("RemoveUnlockKey", func(t *testing.T) {
 | 
				
			||||||
 | 
							// Ensure key directory exists before removal
 | 
				
			||||||
 | 
							keyExists, err := afero.DirExists(fs, keyDir)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to check if key directory exists: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if !keyExists {
 | 
				
			||||||
 | 
								t.Fatalf("Key directory does not exist: %s", keyDir)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Remove unlock key
 | 
				
			||||||
 | 
							err = unlockKey.Remove()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to remove unlock key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify directory is gone
 | 
				
			||||||
 | 
							keyExists, err = afero.DirExists(fs, keyDir)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to check if key directory exists: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if keyExists {
 | 
				
			||||||
 | 
								t.Errorf("Key directory still exists after removal: %s", keyDir)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -149,9 +149,9 @@ func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
 | 
				
			|||||||
		return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Decrypt the vault's long-term private key using the unlock key
 | 
						// Decrypt the encrypted long-term private key using the unlock key
 | 
				
			||||||
	Debug("Decrypting vault's long-term private key with unlock key", "secret_name", s.Name)
 | 
						Debug("Decrypting long-term private key using unlock key", "secret_name", s.Name)
 | 
				
			||||||
	ltPrivKeyData, err := decryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
 | 
						ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt long-term private key", "error", err, "secret_name", s.Name)
 | 
							Debug("Failed to decrypt long-term private key", "error", err, "secret_name", s.Name)
 | 
				
			||||||
		return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
@ -198,11 +198,11 @@ func (s *Secret) decryptWithLongTermKey(ltIdentity *age.X25519Identity) ([]byte,
 | 
				
			|||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Step 2: Decrypt the secret's private key using the vault's long-term private key
 | 
						// Step 2: Decrypt the secret's private key using the vault's long-term private key
 | 
				
			||||||
	Debug("Decrypting secret's private key with vault's long-term key", "secret_name", s.Name)
 | 
						Debug("Decrypting secret private key using long-term key", "secret_name", s.Name)
 | 
				
			||||||
	secretPrivKeyData, err := decryptWithIdentity(encryptedSecretPrivKey, ltIdentity)
 | 
						secretPrivKeyData, err := DecryptWithIdentity(encryptedSecretPrivKey, ltIdentity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt secret's private key", "error", err, "secret_name", s.Name)
 | 
							Debug("Failed to decrypt secret private key", "error", err, "secret_name", s.Name)
 | 
				
			||||||
		return nil, fmt.Errorf("failed to decrypt secret's private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to decrypt secret private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Parse the secret's private key
 | 
						// Parse the secret's private key
 | 
				
			||||||
@ -234,8 +234,8 @@ func (s *Secret) decryptWithLongTermKey(ltIdentity *age.X25519Identity) ([]byte,
 | 
				
			|||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Step 4: Decrypt the secret's value using the secret's private key
 | 
						// Step 4: Decrypt the secret's value using the secret's private key
 | 
				
			||||||
	Debug("Decrypting secret value with secret's private key", "secret_name", s.Name)
 | 
						Debug("Decrypting value using secret key", "secret_name", s.Name)
 | 
				
			||||||
	decryptedValue, err := decryptWithIdentity(encryptedValue, secretIdentity)
 | 
						decryptedValue, err := DecryptWithIdentity(encryptedValue, secretIdentity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt secret value", "error", err, "secret_name", s.Name)
 | 
							Debug("Failed to decrypt secret value", "error", err, "secret_name", s.Name)
 | 
				
			||||||
		return nil, fmt.Errorf("failed to decrypt secret value: %w", err)
 | 
							return nil, fmt.Errorf("failed to decrypt secret value: %w", err)
 | 
				
			||||||
 | 
				
			|||||||
@ -25,10 +25,10 @@ func (m *MockVault) GetDirectory() (string, error) {
 | 
				
			|||||||
func (m *MockVault) AddSecret(name string, value []byte, force bool) error {
 | 
					func (m *MockVault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			||||||
	// Simplified implementation for testing
 | 
						// Simplified implementation for testing
 | 
				
			||||||
	secretDir := filepath.Join(m.directory, "secrets.d", name)
 | 
						secretDir := filepath.Join(m.directory, "secrets.d", name)
 | 
				
			||||||
	if err := m.fs.MkdirAll(secretDir, 0700); err != nil {
 | 
						if err := m.fs.MkdirAll(secretDir, DirPerms); err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return afero.WriteFile(m.fs, filepath.Join(secretDir, "value.age"), value, 0600)
 | 
						return afero.WriteFile(m.fs, filepath.Join(secretDir, "value.age"), value, FilePerms)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (m *MockVault) GetName() string {
 | 
					func (m *MockVault) GetName() string {
 | 
				
			||||||
@ -70,7 +70,7 @@ func TestPerSecretKeyFunctionality(t *testing.T) {
 | 
				
			|||||||
	vaultDir := filepath.Join(baseDir, "vaults.d", "test-vault")
 | 
						vaultDir := filepath.Join(baseDir, "vaults.d", "test-vault")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create vault directory structure
 | 
						// Create vault directory structure
 | 
				
			||||||
	err := fs.MkdirAll(filepath.Join(vaultDir, "secrets.d"), 0700)
 | 
						err := fs.MkdirAll(filepath.Join(vaultDir, "secrets.d"), DirPerms)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		t.Fatalf("Failed to create vault directory: %v", err)
 | 
							t.Fatalf("Failed to create vault directory: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -95,7 +95,7 @@ func TestPerSecretKeyFunctionality(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Set current vault
 | 
						// Set current vault
 | 
				
			||||||
	currentVaultPath := filepath.Join(baseDir, "currentvault")
 | 
						currentVaultPath := filepath.Join(baseDir, "currentvault")
 | 
				
			||||||
	err = afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), 0600)
 | 
						err = afero.WriteFile(fs, currentVaultPath, []byte(vaultDir), FilePerms)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		t.Fatalf("Failed to set current vault: %v", err)
 | 
							t.Fatalf("Failed to set current vault: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										425
									
								
								internal/vault/integration_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										425
									
								
								internal/vault/integration_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,425 @@
 | 
				
			|||||||
 | 
					package vault_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/internal/secret"
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/internal/vault"
 | 
				
			||||||
 | 
						"git.eeqj.de/sneak/secret/pkg/agehd"
 | 
				
			||||||
 | 
						"github.com/spf13/afero"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestVaultWithRealFilesystem(t *testing.T) {
 | 
				
			||||||
 | 
						// Create a temporary directory for our tests
 | 
				
			||||||
 | 
						tempDir, err := os.MkdirTemp("", "secret-test-")
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							t.Fatalf("Failed to create temp dir: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer os.RemoveAll(tempDir) // Clean up after test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Use the real filesystem
 | 
				
			||||||
 | 
						fs := afero.NewOsFs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test mnemonic
 | 
				
			||||||
 | 
						testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Save original environment variables
 | 
				
			||||||
 | 
						oldMnemonic := os.Getenv(secret.EnvMnemonic)
 | 
				
			||||||
 | 
						oldPassphrase := os.Getenv(secret.EnvUnlockPassphrase)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Set test environment variables
 | 
				
			||||||
 | 
						os.Setenv(secret.EnvMnemonic, testMnemonic)
 | 
				
			||||||
 | 
						os.Setenv(secret.EnvUnlockPassphrase, "test-passphrase")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Clean up after test
 | 
				
			||||||
 | 
						defer func() {
 | 
				
			||||||
 | 
							if oldMnemonic != "" {
 | 
				
			||||||
 | 
								os.Setenv(secret.EnvMnemonic, oldMnemonic)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Unsetenv(secret.EnvMnemonic)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if oldPassphrase != "" {
 | 
				
			||||||
 | 
								os.Setenv(secret.EnvUnlockPassphrase, oldPassphrase)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								os.Unsetenv(secret.EnvUnlockPassphrase)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test symlink handling
 | 
				
			||||||
 | 
						t.Run("SymlinkHandling", func(t *testing.T) {
 | 
				
			||||||
 | 
							stateDir := filepath.Join(tempDir, "symlink-test")
 | 
				
			||||||
 | 
							if err := os.MkdirAll(stateDir, 0700); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create state dir: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create a test vault
 | 
				
			||||||
 | 
							vlt, err := vault.CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get the vault directory
 | 
				
			||||||
 | 
							vaultDir, err := vlt.GetDirectory()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to get vault directory: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create a symlink to the vault directory in a different location
 | 
				
			||||||
 | 
							symlinkPath := filepath.Join(tempDir, "test-symlink")
 | 
				
			||||||
 | 
							if err := os.Symlink(vaultDir, symlinkPath); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create symlink: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Test that we can resolve the symlink correctly
 | 
				
			||||||
 | 
							resolvedPath, err := vault.ResolveVaultSymlink(fs, symlinkPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to resolve symlink: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// On some platforms, the resolved path might have different case or format
 | 
				
			||||||
 | 
							// We'll use filepath.EvalSymlinks to get the canonical path for comparison
 | 
				
			||||||
 | 
							expectedPath, err := filepath.EvalSymlinks(vaultDir)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to evaluate symlink: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							actualPath, err := filepath.EvalSymlinks(resolvedPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to evaluate resolved path: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if actualPath != expectedPath {
 | 
				
			||||||
 | 
								t.Errorf("Expected symlink to resolve to %s, got %s", expectedPath, actualPath)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test secret operations with deeply nested paths
 | 
				
			||||||
 | 
						t.Run("DeepPathSecrets", func(t *testing.T) {
 | 
				
			||||||
 | 
							stateDir := filepath.Join(tempDir, "deep-path-test")
 | 
				
			||||||
 | 
							if err := os.MkdirAll(stateDir, 0700); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create state dir: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create a test vault
 | 
				
			||||||
 | 
							vlt, err := vault.CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Derive long-term key from mnemonic
 | 
				
			||||||
 | 
							ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to derive long-term key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get the vault directory
 | 
				
			||||||
 | 
							vaultDir, err := vlt.GetDirectory()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to get vault directory: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Write long-term public key
 | 
				
			||||||
 | 
							ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
 | 
				
			||||||
 | 
							pubKey := ltIdentity.Recipient().String()
 | 
				
			||||||
 | 
							if err := afero.WriteFile(fs, ltPubKeyPath, []byte(pubKey), secret.FilePerms); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to write long-term public key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Unlock the vault
 | 
				
			||||||
 | 
							vlt.Unlock(ltIdentity)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create a secret with a deeply nested path
 | 
				
			||||||
 | 
							deepPath := "api/credentials/production/database/primary"
 | 
				
			||||||
 | 
							secretValue := []byte("supersecretdbpassword")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err = vlt.AddSecret(deepPath, secretValue, false)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to add secret with deep path: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// List secrets and verify our deep path secret is there
 | 
				
			||||||
 | 
							secrets, err := vlt.ListSecrets()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to list secrets: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							found := false
 | 
				
			||||||
 | 
							for _, s := range secrets {
 | 
				
			||||||
 | 
								if s == deepPath {
 | 
				
			||||||
 | 
									found = true
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !found {
 | 
				
			||||||
 | 
								t.Errorf("Deep path secret not found in listed secrets")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Retrieve the secret and verify its value
 | 
				
			||||||
 | 
							retrievedValue, err := vlt.GetSecret(deepPath)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to retrieve deep path secret: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if string(retrievedValue) != string(secretValue) {
 | 
				
			||||||
 | 
								t.Errorf("Retrieved value doesn't match. Expected %q, got %q",
 | 
				
			||||||
 | 
									string(secretValue), string(retrievedValue))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test key caching in GetOrDeriveLongTermKey
 | 
				
			||||||
 | 
						t.Run("KeyCaching", func(t *testing.T) {
 | 
				
			||||||
 | 
							stateDir := filepath.Join(tempDir, "key-cache-test")
 | 
				
			||||||
 | 
							if err := os.MkdirAll(stateDir, 0700); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create state dir: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create a test vault
 | 
				
			||||||
 | 
							vlt, err := vault.CreateVault(fs, stateDir, "test-vault")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create vault: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Derive long-term key from mnemonic
 | 
				
			||||||
 | 
							ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to derive long-term key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get the vault directory
 | 
				
			||||||
 | 
							vaultDir, err := vlt.GetDirectory()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to get vault directory: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Write long-term public key
 | 
				
			||||||
 | 
							ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
 | 
				
			||||||
 | 
							pubKey := ltIdentity.Recipient().String()
 | 
				
			||||||
 | 
							if err := afero.WriteFile(fs, ltPubKeyPath, []byte(pubKey), secret.FilePerms); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to write long-term public key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the vault is locked initially
 | 
				
			||||||
 | 
							if !vlt.Locked() {
 | 
				
			||||||
 | 
								t.Errorf("Vault should be locked initially")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// First call to GetOrDeriveLongTermKey should derive and cache the key
 | 
				
			||||||
 | 
							firstKey, err := vlt.GetOrDeriveLongTermKey()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to get long-term key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the vault is now unlocked
 | 
				
			||||||
 | 
							if vlt.Locked() {
 | 
				
			||||||
 | 
								t.Errorf("Vault should be unlocked after GetOrDeriveLongTermKey")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Second call should return the cached key without re-deriving
 | 
				
			||||||
 | 
							secondKey, err := vlt.GetOrDeriveLongTermKey()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to get cached long-term key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify both keys are the same instance
 | 
				
			||||||
 | 
							if firstKey != secondKey {
 | 
				
			||||||
 | 
								t.Errorf("Second key call should return same instance as first call")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the public key matches what we expect
 | 
				
			||||||
 | 
							expectedPubKey := ltIdentity.Recipient().String()
 | 
				
			||||||
 | 
							actualPubKey := firstKey.Recipient().String()
 | 
				
			||||||
 | 
							if actualPubKey != expectedPubKey {
 | 
				
			||||||
 | 
								t.Errorf("Public key mismatch. Expected %s, got %s", expectedPubKey, actualPubKey)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Now clear the key and verify it's locked again
 | 
				
			||||||
 | 
							vlt.ClearLongTermKey()
 | 
				
			||||||
 | 
							if !vlt.Locked() {
 | 
				
			||||||
 | 
								t.Errorf("Vault should be locked after clearing key")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Get the key again and verify it works
 | 
				
			||||||
 | 
							thirdKey, err := vlt.GetOrDeriveLongTermKey()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to re-derive long-term key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the public key still matches
 | 
				
			||||||
 | 
							actualPubKey = thirdKey.Recipient().String()
 | 
				
			||||||
 | 
							if actualPubKey != expectedPubKey {
 | 
				
			||||||
 | 
								t.Errorf("Re-derived public key mismatch. Expected %s, got %s", expectedPubKey, actualPubKey)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test vault name validation
 | 
				
			||||||
 | 
						t.Run("VaultNameValidation", func(t *testing.T) {
 | 
				
			||||||
 | 
							stateDir := filepath.Join(tempDir, "name-validation-test")
 | 
				
			||||||
 | 
							if err := os.MkdirAll(stateDir, 0700); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create state dir: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Test valid vault names
 | 
				
			||||||
 | 
							validNames := []string{
 | 
				
			||||||
 | 
								"default",
 | 
				
			||||||
 | 
								"test-vault",
 | 
				
			||||||
 | 
								"production.vault",
 | 
				
			||||||
 | 
								"vault_123",
 | 
				
			||||||
 | 
								"a-very-long-vault-name-with-dashes",
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, name := range validNames {
 | 
				
			||||||
 | 
								_, err := vault.CreateVault(fs, stateDir, name)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Errorf("Failed to create vault with valid name %q: %v", name, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Test invalid vault names
 | 
				
			||||||
 | 
							invalidNames := []string{
 | 
				
			||||||
 | 
								"",             // Empty
 | 
				
			||||||
 | 
								"UPPERCASE",    // Uppercase not allowed
 | 
				
			||||||
 | 
								"invalid/name", // Slashes not allowed in vault names
 | 
				
			||||||
 | 
								"invalid name", // Spaces not allowed
 | 
				
			||||||
 | 
								"invalid@name", // Special chars not allowed
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for _, name := range invalidNames {
 | 
				
			||||||
 | 
								_, err := vault.CreateVault(fs, stateDir, name)
 | 
				
			||||||
 | 
								if err == nil {
 | 
				
			||||||
 | 
									t.Errorf("Expected error creating vault with invalid name %q, but got none", name)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test multiple vaults and switching between them
 | 
				
			||||||
 | 
						t.Run("MultipleVaults", func(t *testing.T) {
 | 
				
			||||||
 | 
							stateDir := filepath.Join(tempDir, "multi-vault-test")
 | 
				
			||||||
 | 
							if err := os.MkdirAll(stateDir, 0700); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create state dir: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create three vaults
 | 
				
			||||||
 | 
							vaultNames := []string{"vault1", "vault2", "vault3"}
 | 
				
			||||||
 | 
							for _, name := range vaultNames {
 | 
				
			||||||
 | 
								_, err := vault.CreateVault(fs, stateDir, name)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("Failed to create vault %s: %v", name, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// List vaults and verify all three are there
 | 
				
			||||||
 | 
							vaults, err := vault.ListVaults(fs, stateDir)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to list vaults: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if len(vaults) != 3 {
 | 
				
			||||||
 | 
								t.Errorf("Expected 3 vaults, got %d", len(vaults))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Test switching between vaults
 | 
				
			||||||
 | 
							for _, name := range vaultNames {
 | 
				
			||||||
 | 
								// Select the vault
 | 
				
			||||||
 | 
								if err := vault.SelectVault(fs, stateDir, name); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("Failed to select vault %s: %v", name, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Get current vault and verify it's the one we selected
 | 
				
			||||||
 | 
								currentVault, err := vault.GetCurrentVault(fs, stateDir)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("Failed to get current vault after selecting %s: %v", name, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if currentVault.GetName() != name {
 | 
				
			||||||
 | 
									t.Errorf("Expected current vault to be %s, got %s", name, currentVault.GetName())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Test adding a secret in one vault and verifying it's not visible in another
 | 
				
			||||||
 | 
						t.Run("VaultIsolation", func(t *testing.T) {
 | 
				
			||||||
 | 
							stateDir := filepath.Join(tempDir, "isolation-test")
 | 
				
			||||||
 | 
							if err := os.MkdirAll(stateDir, 0700); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create state dir: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Create two vaults
 | 
				
			||||||
 | 
							vault1, err := vault.CreateVault(fs, stateDir, "vault1")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create vault1: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							vault2, err := vault.CreateVault(fs, stateDir, "vault2")
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to create vault2: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Derive long-term key from mnemonic
 | 
				
			||||||
 | 
							ltIdentity, err := agehd.DeriveIdentity(testMnemonic, 0)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to derive long-term key: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Setup both vaults with the same long-term key
 | 
				
			||||||
 | 
							for _, vlt := range []*vault.Vault{vault1, vault2} {
 | 
				
			||||||
 | 
								vaultDir, err := vlt.GetDirectory()
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("Failed to get vault directory: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								ltPubKeyPath := filepath.Join(vaultDir, "pub.age")
 | 
				
			||||||
 | 
								pubKey := ltIdentity.Recipient().String()
 | 
				
			||||||
 | 
								if err := afero.WriteFile(fs, ltPubKeyPath, []byte(pubKey), secret.FilePerms); err != nil {
 | 
				
			||||||
 | 
									t.Fatalf("Failed to write long-term public key: %v", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								vlt.Unlock(ltIdentity)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Add a secret to vault1
 | 
				
			||||||
 | 
							secretName := "test-secret"
 | 
				
			||||||
 | 
							secretValue := []byte("secret in vault1")
 | 
				
			||||||
 | 
							if err := vault1.AddSecret(secretName, secretValue, false); err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to add secret to vault1: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the secret exists in vault1
 | 
				
			||||||
 | 
							vault1Secrets, err := vault1.ListSecrets()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to list secrets in vault1: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							found := false
 | 
				
			||||||
 | 
							for _, s := range vault1Secrets {
 | 
				
			||||||
 | 
								if s == secretName {
 | 
				
			||||||
 | 
									found = true
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !found {
 | 
				
			||||||
 | 
								t.Errorf("Secret not found in vault1")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Verify the secret does NOT exist in vault2
 | 
				
			||||||
 | 
							vault2Secrets, err := vault2.ListSecrets()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								t.Fatalf("Failed to list secrets in vault2: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							found = false
 | 
				
			||||||
 | 
							for _, s := range vault2Secrets {
 | 
				
			||||||
 | 
								if s == secretName {
 | 
				
			||||||
 | 
									found = true
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if found {
 | 
				
			||||||
 | 
								t.Errorf("Secret from vault1 should not be visible in vault2")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -27,115 +27,149 @@ func isValidVaultName(name string) bool {
 | 
				
			|||||||
	return matched
 | 
						return matched
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// resolveVaultSymlink resolves the currentvault symlink by reading either the symlink target or file contents
 | 
					// ResolveVaultSymlink resolves the currentvault symlink by reading either the symlink target or file contents
 | 
				
			||||||
// This function is designed to work on both Unix and Windows systems, as well as with in-memory filesystems
 | 
					// This function is designed to work on both Unix and Windows systems, as well as with in-memory filesystems
 | 
				
			||||||
func resolveVaultSymlink(fs afero.Fs, symlinkPath string) (string, error) {
 | 
					func ResolveVaultSymlink(fs afero.Fs, symlinkPath string) (string, error) {
 | 
				
			||||||
	secret.Debug("resolveVaultSymlink starting", "symlink_path", symlinkPath)
 | 
						secret.Debug("resolveVaultSymlink starting", "symlink_path", symlinkPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// First try to handle the path as a real symlink (works on Unix systems)
 | 
						// First try to handle the path as a real symlink (works on Unix systems)
 | 
				
			||||||
	if _, ok := fs.(*afero.OsFs); ok {
 | 
						if _, ok := fs.(*afero.OsFs); ok {
 | 
				
			||||||
		secret.Debug("Trying real filesystem symlink resolution")
 | 
							secret.Debug("Using real filesystem symlink resolution")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Check if it's a real symlink first (will work on Unix)
 | 
							// Check if the symlink exists
 | 
				
			||||||
		linkTarget, err := os.Readlink(symlinkPath)
 | 
							secret.Debug("Checking symlink target", "symlink_path", symlinkPath)
 | 
				
			||||||
 | 
							target, err := os.Readlink(symlinkPath)
 | 
				
			||||||
		if err == nil {
 | 
							if err == nil {
 | 
				
			||||||
			// Successfully read as symlink (Unix path)
 | 
								secret.Debug("Symlink points to", "target", target)
 | 
				
			||||||
			secret.Debug("Successfully read as real symlink", "target", linkTarget)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Convert relative paths to absolute if needed
 | 
								// On real filesystem, we need to handle relative symlinks
 | 
				
			||||||
			if !filepath.IsAbs(linkTarget) {
 | 
								// by resolving them relative to the symlink's directory
 | 
				
			||||||
				linkTarget = filepath.Join(filepath.Dir(symlinkPath), linkTarget)
 | 
								if !filepath.IsAbs(target) {
 | 
				
			||||||
			}
 | 
									// Get the current directory before changing
 | 
				
			||||||
 | 
									originalDir, err := os.Getwd()
 | 
				
			||||||
			secret.Debug("Resolved symlink path", "path", linkTarget)
 | 
					 | 
				
			||||||
			return linkTarget, nil
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		secret.Debug("Not a real symlink or on Windows, trying as regular file", "error", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// For Windows or in-memory filesystems, or when symlink reading fails:
 | 
					 | 
				
			||||||
	// Read the path from a regular file (our fallback approach)
 | 
					 | 
				
			||||||
	secret.Debug("Reading symlink target from file")
 | 
					 | 
				
			||||||
	content, err := afero.ReadFile(fs, symlinkPath)
 | 
					 | 
				
			||||||
				if err != nil {
 | 
									if err != nil {
 | 
				
			||||||
		secret.Debug("Failed to read from file", "error", err)
 | 
										return "", fmt.Errorf("failed to get current directory: %w", err)
 | 
				
			||||||
		return "", fmt.Errorf("failed to read vault path: %w", err)
 | 
									}
 | 
				
			||||||
 | 
									secret.Debug("Got current directory", "original_dir", originalDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Change to the symlink's directory
 | 
				
			||||||
 | 
									symlinkDir := filepath.Dir(symlinkPath)
 | 
				
			||||||
 | 
									secret.Debug("Changing to symlink directory", "symlink_path", symlinkDir)
 | 
				
			||||||
 | 
									secret.Debug("About to call os.Chdir - this might hang if symlink is broken")
 | 
				
			||||||
 | 
									if err := os.Chdir(symlinkDir); err != nil {
 | 
				
			||||||
 | 
										return "", fmt.Errorf("failed to change to symlink directory: %w", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									secret.Debug("Changed to symlink directory successfully - os.Chdir completed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Get the absolute path of the target
 | 
				
			||||||
 | 
									secret.Debug("Getting absolute path of current directory")
 | 
				
			||||||
 | 
									absolutePath, err := os.Getwd()
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										// Try to restore original directory before returning error
 | 
				
			||||||
 | 
										_ = os.Chdir(originalDir)
 | 
				
			||||||
 | 
										return "", fmt.Errorf("failed to get absolute path: %w", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									secret.Debug("Got absolute path", "absolute_path", absolutePath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Restore the original directory
 | 
				
			||||||
 | 
									secret.Debug("Restoring original directory", "original_dir", originalDir)
 | 
				
			||||||
 | 
									if err := os.Chdir(originalDir); err != nil {
 | 
				
			||||||
 | 
										return "", fmt.Errorf("failed to restore original directory: %w", err)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									secret.Debug("Restored original directory successfully")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									// Use the absolute path of the target
 | 
				
			||||||
 | 
									target = absolutePath
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	targetPath := string(content)
 | 
								secret.Debug("resolveVaultSymlink completed successfully", "result", target)
 | 
				
			||||||
	secret.Debug("Read target path from file", "target_path", targetPath)
 | 
								return target, nil
 | 
				
			||||||
	return targetPath, nil
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetCurrentVault gets the currently selected vault
 | 
						// Fallback: treat it as a regular file containing the target path
 | 
				
			||||||
 | 
						secret.Debug("Fallback: trying to read regular file with target path")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fileData, err := afero.ReadFile(fs, symlinkPath)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							secret.Debug("Failed to read target path file", "error", err)
 | 
				
			||||||
 | 
							return "", fmt.Errorf("failed to read vault symlink: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						target := string(fileData)
 | 
				
			||||||
 | 
						secret.Debug("Read target path from file", "target", target)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						secret.Debug("resolveVaultSymlink completed via fallback", "result", target)
 | 
				
			||||||
 | 
						return target, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetCurrentVault gets the current vault from the file system
 | 
				
			||||||
func GetCurrentVault(fs afero.Fs, stateDir string) (*Vault, error) {
 | 
					func GetCurrentVault(fs afero.Fs, stateDir string) (*Vault, error) {
 | 
				
			||||||
	secret.Debug("Getting current vault", "state_dir", stateDir)
 | 
						secret.Debug("Getting current vault", "state_dir", stateDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if current vault symlink exists
 | 
						// Check if the current vault symlink exists
 | 
				
			||||||
	currentVaultPath := filepath.Join(stateDir, "currentvault")
 | 
						currentVaultPath := filepath.Join(stateDir, "currentvault")
 | 
				
			||||||
	secret.Debug("Checking current vault symlink", "path", currentVaultPath)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						secret.Debug("Checking current vault symlink", "path", currentVaultPath)
 | 
				
			||||||
	_, err := fs.Stat(currentVaultPath)
 | 
						_, err := fs.Stat(currentVaultPath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		secret.Debug("Failed to stat current vault symlink", "error", err, "path", currentVaultPath)
 | 
							secret.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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	secret.Debug("Current vault symlink exists")
 | 
						secret.Debug("Current vault symlink exists")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Resolve symlink to get target path
 | 
						// Resolve the symlink to get the actual vault directory
 | 
				
			||||||
	secret.Debug("Resolving vault symlink")
 | 
						secret.Debug("Resolving vault symlink")
 | 
				
			||||||
	targetPath, err := resolveVaultSymlink(fs, currentVaultPath)
 | 
						targetPath, err := ResolveVaultSymlink(fs, currentVaultPath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		secret.Debug("Failed to resolve vault symlink", "error", err)
 | 
							return nil, err
 | 
				
			||||||
		return nil, fmt.Errorf("failed to resolve vault symlink: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	secret.Debug("Resolved vault symlink", "target_path", targetPath)
 | 
						secret.Debug("Resolved vault symlink", "target_path", targetPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Extract vault name from target path
 | 
						// Extract the vault name from the path
 | 
				
			||||||
 | 
						// The path will be something like "/path/to/vaults.d/default"
 | 
				
			||||||
	vaultName := filepath.Base(targetPath)
 | 
						vaultName := filepath.Base(targetPath)
 | 
				
			||||||
	secret.Debug("Extracted vault name", "vault_name", vaultName)
 | 
						secret.Debug("Extracted vault name", "vault_name", vaultName)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	secret.Debug("Current vault resolved", "vault_name", vaultName, "target_path", targetPath)
 | 
						secret.Debug("Current vault resolved", "vault_name", vaultName, "target_path", targetPath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create and return Vault instance
 | 
						// Create and return the vault
 | 
				
			||||||
	secret.Debug("Creating NewVault instance")
 | 
						return NewVault(fs, stateDir, vaultName), nil
 | 
				
			||||||
	vault := NewVault(fs, vaultName, stateDir)
 | 
					 | 
				
			||||||
	secret.Debug("Created NewVault instance successfully")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return vault, nil
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ListVaults returns a list of available vault names
 | 
					// ListVaults lists all vaults in the state directory
 | 
				
			||||||
func ListVaults(fs afero.Fs, stateDir string) ([]string, error) {
 | 
					func ListVaults(fs afero.Fs, stateDir string) ([]string, error) {
 | 
				
			||||||
	vaultsDir := filepath.Join(stateDir, "vaults.d")
 | 
						vaultsDir := filepath.Join(stateDir, "vaults.d")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if vaults directory exists
 | 
						// Check if vaults directory exists
 | 
				
			||||||
	exists, err := afero.DirExists(fs, vaultsDir)
 | 
						exists, err := afero.DirExists(fs, vaultsDir)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to check vaults directory: %w", err)
 | 
							return nil, fmt.Errorf("failed to check if vaults directory exists: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if !exists {
 | 
						if !exists {
 | 
				
			||||||
		return []string{}, nil
 | 
							return []string{}, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Read directory contents
 | 
						// Read the vaults directory
 | 
				
			||||||
	files, err := afero.ReadDir(fs, vaultsDir)
 | 
						entries, err := afero.ReadDir(fs, vaultsDir)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to read vaults directory: %w", err)
 | 
							return nil, fmt.Errorf("failed to read vaults directory: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Extract vault names
 | 
				
			||||||
	var vaults []string
 | 
						var vaults []string
 | 
				
			||||||
	for _, file := range files {
 | 
						for _, entry := range entries {
 | 
				
			||||||
		if file.IsDir() {
 | 
							if entry.IsDir() {
 | 
				
			||||||
			vaults = append(vaults, file.Name())
 | 
								vaults = append(vaults, entry.Name())
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return vaults, nil
 | 
						return vaults, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateVault creates a new vault with the given name
 | 
					// CreateVault creates a new vault
 | 
				
			||||||
func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
 | 
					func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
 | 
				
			||||||
	secret.Debug("Creating new vault", "name", name, "state_dir", stateDir)
 | 
						secret.Debug("Creating new vault", "name", name, "state_dir", stateDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -150,39 +184,32 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
 | 
				
			|||||||
	vaultDir := filepath.Join(stateDir, "vaults.d", name)
 | 
						vaultDir := filepath.Join(stateDir, "vaults.d", name)
 | 
				
			||||||
	secret.Debug("Creating vault directory structure", "vault_dir", vaultDir)
 | 
						secret.Debug("Creating vault directory structure", "vault_dir", vaultDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check if vault already exists
 | 
						// Create main vault directory
 | 
				
			||||||
	exists, err := afero.DirExists(fs, vaultDir)
 | 
						if err := fs.MkdirAll(vaultDir, secret.DirPerms); err != nil {
 | 
				
			||||||
	if err != nil {
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("failed to check if vault exists: %w", err)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if exists {
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("vault %s already exists", name)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Create vault directory
 | 
					 | 
				
			||||||
	if err := fs.MkdirAll(vaultDir, 0700); err != nil {
 | 
					 | 
				
			||||||
		return nil, fmt.Errorf("failed to create vault directory: %w", err)
 | 
							return nil, fmt.Errorf("failed to create vault directory: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create subdirectories
 | 
						// Create secrets directory
 | 
				
			||||||
	secretsDir := filepath.Join(vaultDir, "secrets.d")
 | 
						secretsDir := filepath.Join(vaultDir, "secrets.d")
 | 
				
			||||||
	if err := fs.MkdirAll(secretsDir, 0700); err != nil {
 | 
						if err := fs.MkdirAll(secretsDir, secret.DirPerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create secrets directory: %w", err)
 | 
							return nil, fmt.Errorf("failed to create secrets directory: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create unlock keys directory
 | 
				
			||||||
	unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
 | 
						unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
 | 
				
			||||||
	if err := fs.MkdirAll(unlockKeysDir, 0700); err != nil {
 | 
						if err := fs.MkdirAll(unlockKeysDir, secret.DirPerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create unlock keys directory: %w", err)
 | 
							return nil, fmt.Errorf("failed to create unlock keys directory: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Select the new vault as current
 | 
						// Select the newly created vault as current
 | 
				
			||||||
	secret.Debug("Selecting newly created vault as current", "name", name)
 | 
						secret.Debug("Selecting newly created vault as current", "name", name)
 | 
				
			||||||
	if err := SelectVault(fs, stateDir, name); err != nil {
 | 
						if err := SelectVault(fs, stateDir, name); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to select new vault: %w", err)
 | 
							return nil, fmt.Errorf("failed to select vault: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create and return the vault
 | 
				
			||||||
	secret.Debug("Successfully created vault", "name", name)
 | 
						secret.Debug("Successfully created vault", "name", name)
 | 
				
			||||||
	return NewVault(fs, name, stateDir), nil
 | 
						return NewVault(fs, stateDir, name), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SelectVault selects the given vault as the current vault
 | 
					// SelectVault selects the given vault as the current vault
 | 
				
			||||||
@ -206,35 +233,32 @@ func SelectVault(fs afero.Fs, stateDir string, name string) error {
 | 
				
			|||||||
		return fmt.Errorf("vault %s does not exist", name)
 | 
							return fmt.Errorf("vault %s does not exist", name)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create/update current vault symlink
 | 
						// Create or update the current vault symlink/file
 | 
				
			||||||
	currentVaultPath := filepath.Join(stateDir, "currentvault")
 | 
						currentVaultPath := filepath.Join(stateDir, "currentvault")
 | 
				
			||||||
 | 
						targetPath := filepath.Join(stateDir, "vaults.d", name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Remove existing symlink if it exists
 | 
						// First try to remove existing symlink if it exists
 | 
				
			||||||
	if exists, _ := afero.Exists(fs, currentVaultPath); exists {
 | 
						if _, err := fs.Stat(currentVaultPath); err == nil {
 | 
				
			||||||
		secret.Debug("Removing existing current vault symlink", "path", currentVaultPath)
 | 
							secret.Debug("Removing existing current vault symlink", "path", currentVaultPath)
 | 
				
			||||||
		if err := fs.Remove(currentVaultPath); err != nil {
 | 
							// Ignore errors from Remove as we'll try to create/update it anyway.
 | 
				
			||||||
			secret.Debug("Failed to remove existing symlink", "error", err, "path", currentVaultPath)
 | 
							// On some systems, removing a symlink may fail but the subsequent create may still succeed.
 | 
				
			||||||
		}
 | 
							_ = fs.Remove(currentVaultPath)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create new symlink pointing to the vault
 | 
						// Try to create a real symlink first (works on Unix systems)
 | 
				
			||||||
	targetPath := vaultDir
 | 
					 | 
				
			||||||
	secret.Debug("Creating vault symlink", "target", targetPath, "link", currentVaultPath)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// For real filesystems, try to create a real symlink first
 | 
					 | 
				
			||||||
	if _, ok := fs.(*afero.OsFs); ok {
 | 
						if _, ok := fs.(*afero.OsFs); ok {
 | 
				
			||||||
		if err := os.Symlink(targetPath, currentVaultPath); err != nil {
 | 
							secret.Debug("Creating vault symlink", "target", targetPath, "link", currentVaultPath)
 | 
				
			||||||
			// If symlink creation fails, fall back to writing target path to file
 | 
							if err := os.Symlink(targetPath, currentVaultPath); err == nil {
 | 
				
			||||||
			secret.Debug("Failed to create real symlink, falling back to file", "error", err)
 | 
								secret.Debug("Successfully selected vault", "vault_name", name)
 | 
				
			||||||
			if err := afero.WriteFile(fs, currentVaultPath, []byte(targetPath), 0600); err != nil {
 | 
								return nil
 | 
				
			||||||
				return fmt.Errorf("failed to create vault symlink: %w", err)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							// If symlink creation fails, fall back to regular file
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	} else {
 | 
					
 | 
				
			||||||
		// For in-memory filesystems, write target path to file
 | 
						// Fallback: create a regular file with the target path
 | 
				
			||||||
		if err := afero.WriteFile(fs, currentVaultPath, []byte(targetPath), 0600); err != nil {
 | 
						secret.Debug("Fallback: creating regular file with target path", "target", targetPath)
 | 
				
			||||||
			return fmt.Errorf("failed to create vault symlink: %w", err)
 | 
						if err := afero.WriteFile(fs, currentVaultPath, []byte(targetPath), secret.FilePerms); err != nil {
 | 
				
			||||||
		}
 | 
							return fmt.Errorf("failed to select vault: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	secret.Debug("Successfully selected vault", "vault_name", name)
 | 
						secret.Debug("Successfully selected vault", "vault_name", name)
 | 
				
			||||||
 | 
				
			|||||||
@ -120,7 +120,7 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Create secret directory
 | 
						// Create secret directory
 | 
				
			||||||
	secret.Debug("Creating secret directory", "secret_dir", secretDir)
 | 
						secret.Debug("Creating secret directory", "secret_dir", secretDir)
 | 
				
			||||||
	if err := v.fs.MkdirAll(secretDir, 0700); err != nil {
 | 
						if err := v.fs.MkdirAll(secretDir, secret.DirPerms); err != nil {
 | 
				
			||||||
		secret.Debug("Failed to create secret directory", "error", err, "secret_dir", secretDir)
 | 
							secret.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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -145,7 +145,7 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
	// Step 2: Store the secret's public key
 | 
						// Step 2: Store the secret's public key
 | 
				
			||||||
	pubKeyPath := filepath.Join(secretDir, "pub.age")
 | 
						pubKeyPath := filepath.Join(secretDir, "pub.age")
 | 
				
			||||||
	secret.Debug("Writing secret public key", "path", pubKeyPath)
 | 
						secret.Debug("Writing secret public key", "path", pubKeyPath)
 | 
				
			||||||
	if err := afero.WriteFile(v.fs, pubKeyPath, []byte(secretPublicKey), 0600); err != nil {
 | 
						if err := afero.WriteFile(v.fs, pubKeyPath, []byte(secretPublicKey), secret.FilePerms); err != nil {
 | 
				
			||||||
		secret.Debug("Failed to write secret public key", "error", err, "path", pubKeyPath)
 | 
							secret.Debug("Failed to write secret public key", "error", err, "path", pubKeyPath)
 | 
				
			||||||
		return fmt.Errorf("failed to write secret public key: %w", err)
 | 
							return fmt.Errorf("failed to write secret public key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -167,7 +167,7 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
	// Step 4: Store the encrypted secret value as value.age
 | 
						// Step 4: Store the encrypted secret value as value.age
 | 
				
			||||||
	valuePath := filepath.Join(secretDir, "value.age")
 | 
						valuePath := filepath.Join(secretDir, "value.age")
 | 
				
			||||||
	secret.Debug("Writing encrypted secret value", "path", valuePath)
 | 
						secret.Debug("Writing encrypted secret value", "path", valuePath)
 | 
				
			||||||
	if err := afero.WriteFile(v.fs, valuePath, encryptedValue, 0600); err != nil {
 | 
						if err := afero.WriteFile(v.fs, valuePath, encryptedValue, secret.FilePerms); err != nil {
 | 
				
			||||||
		secret.Debug("Failed to write encrypted secret value", "error", err, "path", valuePath)
 | 
							secret.Debug("Failed to write encrypted secret value", "error", err, "path", valuePath)
 | 
				
			||||||
		return fmt.Errorf("failed to write encrypted secret value: %w", err)
 | 
							return fmt.Errorf("failed to write encrypted secret value: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -209,7 +209,7 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
	// Step 7: Store the encrypted secret private key as priv.age
 | 
						// Step 7: Store the encrypted secret private key as priv.age
 | 
				
			||||||
	privKeyPath := filepath.Join(secretDir, "priv.age")
 | 
						privKeyPath := filepath.Join(secretDir, "priv.age")
 | 
				
			||||||
	secret.Debug("Writing encrypted secret private key", "path", privKeyPath)
 | 
						secret.Debug("Writing encrypted secret private key", "path", privKeyPath)
 | 
				
			||||||
	if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, 0600); err != nil {
 | 
						if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
 | 
				
			||||||
		secret.Debug("Failed to write encrypted secret private key", "error", err, "path", privKeyPath)
 | 
							secret.Debug("Failed to write encrypted secret private key", "error", err, "path", privKeyPath)
 | 
				
			||||||
		return fmt.Errorf("failed to write encrypted secret private key: %w", err)
 | 
							return fmt.Errorf("failed to write encrypted secret private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@ -240,7 +240,7 @@ func (v *Vault) AddSecret(name string, value []byte, force bool) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	metadataPath := filepath.Join(secretDir, "secret-metadata.json")
 | 
						metadataPath := filepath.Join(secretDir, "secret-metadata.json")
 | 
				
			||||||
	secret.Debug("Writing secret metadata", "path", metadataPath)
 | 
						secret.Debug("Writing secret metadata", "path", metadataPath)
 | 
				
			||||||
	if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, 0600); err != nil {
 | 
						if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
 | 
				
			||||||
		secret.Debug("Failed to write secret metadata", "error", err, "path", metadataPath)
 | 
							secret.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)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -37,7 +37,7 @@ func (v *Vault) GetCurrentUnlockKey() (secret.UnlockKey, error) {
 | 
				
			|||||||
	if _, ok := v.fs.(*afero.OsFs); ok {
 | 
						if _, ok := v.fs.(*afero.OsFs); ok {
 | 
				
			||||||
		secret.Debug("Resolving unlock key symlink (real filesystem)")
 | 
							secret.Debug("Resolving unlock key symlink (real filesystem)")
 | 
				
			||||||
		// For real filesystems, resolve the symlink properly
 | 
							// For real filesystems, resolve the symlink properly
 | 
				
			||||||
		unlockKeyDir, err = resolveVaultSymlink(v.fs, currentUnlockKeyPath)
 | 
							unlockKeyDir, err = ResolveVaultSymlink(v.fs, currentUnlockKeyPath)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			secret.Debug("Failed to resolve unlock key symlink", "error", err, "symlink_path", currentUnlockKeyPath)
 | 
								secret.Debug("Failed to resolve unlock key symlink", "error", err, "symlink_path", currentUnlockKeyPath)
 | 
				
			||||||
			return nil, fmt.Errorf("failed to resolve current unlock key symlink: %w", err)
 | 
								return nil, fmt.Errorf("failed to resolve current unlock key symlink: %w", err)
 | 
				
			||||||
@ -288,7 +288,7 @@ func (v *Vault) SelectUnlockKey(keyID string) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create new symlink
 | 
						// Create new symlink
 | 
				
			||||||
	return afero.WriteFile(v.fs, currentUnlockKeyPath, []byte(targetKeyDir), 0600)
 | 
						return afero.WriteFile(v.fs, currentUnlockKeyPath, []byte(targetKeyDir), secret.FilePerms)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreatePassphraseKey creates a new passphrase-protected unlock key
 | 
					// CreatePassphraseKey creates a new passphrase-protected unlock key
 | 
				
			||||||
@ -301,7 +301,7 @@ func (v *Vault) CreatePassphraseKey(passphrase string) (*secret.PassphraseUnlock
 | 
				
			|||||||
	// Create unlock key directory with timestamp
 | 
						// Create unlock key directory with timestamp
 | 
				
			||||||
	timestamp := time.Now().Format("2006-01-02.15.04")
 | 
						timestamp := time.Now().Format("2006-01-02.15.04")
 | 
				
			||||||
	unlockKeyDir := filepath.Join(vaultDir, "unlock.d", "passphrase")
 | 
						unlockKeyDir := filepath.Join(vaultDir, "unlock.d", "passphrase")
 | 
				
			||||||
	if err := v.fs.MkdirAll(unlockKeyDir, 0700); err != nil {
 | 
						if err := v.fs.MkdirAll(unlockKeyDir, secret.DirPerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
 | 
							return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -313,7 +313,7 @@ func (v *Vault) CreatePassphraseKey(passphrase string) (*secret.PassphraseUnlock
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Write public key
 | 
						// Write public key
 | 
				
			||||||
	pubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
 | 
						pubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
 | 
				
			||||||
	if err := afero.WriteFile(v.fs, pubKeyPath, []byte(unlockIdentity.Recipient().String()), 0600); err != nil {
 | 
						if err := afero.WriteFile(v.fs, pubKeyPath, []byte(unlockIdentity.Recipient().String()), secret.FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write unlock key public key: %w", err)
 | 
							return nil, fmt.Errorf("failed to write unlock key public key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -326,7 +326,7 @@ func (v *Vault) CreatePassphraseKey(passphrase string) (*secret.PassphraseUnlock
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Write encrypted private key
 | 
						// Write encrypted private key
 | 
				
			||||||
	privKeyPath := filepath.Join(unlockKeyDir, "priv.age")
 | 
						privKeyPath := filepath.Join(unlockKeyDir, "priv.age")
 | 
				
			||||||
	if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, 0600); err != nil {
 | 
						if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write encrypted unlock key private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to write encrypted unlock key private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -346,8 +346,8 @@ func (v *Vault) CreatePassphraseKey(passphrase string) (*secret.PassphraseUnlock
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	metadataPath := filepath.Join(unlockKeyDir, "unlock-metadata.json")
 | 
						metadataPath := filepath.Join(unlockKeyDir, "unlock-metadata.json")
 | 
				
			||||||
	if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, 0600); err != nil {
 | 
						if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to write metadata: %w", err)
 | 
							return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Encrypt long-term private key to this unlock key if vault is unlocked
 | 
						// Encrypt long-term private key to this unlock key if vault is unlocked
 | 
				
			||||||
@ -359,7 +359,7 @@ func (v *Vault) CreatePassphraseKey(passphrase string) (*secret.PassphraseUnlock
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
 | 
							ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
 | 
				
			||||||
		if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, 0600); err != nil {
 | 
							if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
 | 
				
			||||||
			return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
								return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -21,13 +21,16 @@ type Vault struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 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, stateDir string, name string) *Vault {
 | 
				
			||||||
	return &Vault{
 | 
						secret.Debug("Creating NewVault instance")
 | 
				
			||||||
 | 
						v := &Vault{
 | 
				
			||||||
		Name:        name,
 | 
							Name:        name,
 | 
				
			||||||
		fs:          fs,
 | 
							fs:          fs,
 | 
				
			||||||
		stateDir:    stateDir,
 | 
							stateDir:    stateDir,
 | 
				
			||||||
		longTermKey: nil,
 | 
							longTermKey: nil,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						secret.Debug("Created NewVault instance successfully")
 | 
				
			||||||
 | 
						return v
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Locked returns true if the vault doesn't have a long-term key in memory
 | 
					// Locked returns true if the vault doesn't have a long-term key in memory
 | 
				
			||||||
 | 
				
			|||||||
@ -128,7 +128,7 @@ func TestVaultOperations(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// Write the correct public key to the pub.age file
 | 
							// Write the correct public key to the pub.age file
 | 
				
			||||||
		pubKeyPath := filepath.Join(vaultDir, "pub.age")
 | 
							pubKeyPath := filepath.Join(vaultDir, "pub.age")
 | 
				
			||||||
		err = afero.WriteFile(fs, pubKeyPath, []byte(ltPublicKey), 0600)
 | 
							err = afero.WriteFile(fs, pubKeyPath, []byte(ltPublicKey), secret.FilePerms)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			t.Fatalf("Failed to write long-term public key: %v", err)
 | 
								t.Fatalf("Failed to write long-term public key: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user