secret/internal/vault/management.go

280 lines
9.8 KiB
Go

package vault
import (
"fmt"
"os"
"path/filepath"
"regexp"
"time"
"git.eeqj.de/sneak/secret/internal/secret"
"github.com/spf13/afero"
)
// Register the GetCurrentVault function with the secret package
func init() {
secret.RegisterGetCurrentVaultFunc(func(fs afero.Fs, stateDir string) (secret.VaultInterface, error) {
return GetCurrentVault(fs, stateDir)
})
}
// isValidVaultName validates vault names according to the format [a-z0-9\.\-\_]+
// Note: We don't allow slashes in vault names unlike secret names
func isValidVaultName(name string) bool {
if name == "" {
return false
}
matched, _ := regexp.MatchString(`^[a-z0-9\.\-\_]+$`, name)
return matched
}
// 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
func ResolveVaultSymlink(fs afero.Fs, symlinkPath string) (string, error) {
secret.Debug("resolveVaultSymlink starting", "symlink_path", symlinkPath)
// First try to handle the path as a real symlink (works on Unix systems)
if _, ok := fs.(*afero.OsFs); ok {
secret.Debug("Using real filesystem symlink resolution")
// Check if the symlink exists
secret.Debug("Checking symlink target", "symlink_path", symlinkPath)
target, err := os.Readlink(symlinkPath)
if err == nil {
secret.Debug("Symlink points to", "target", target)
// On real filesystem, we need to handle relative symlinks
// by resolving them relative to the symlink's directory
if !filepath.IsAbs(target) {
// Get the current directory before changing
originalDir, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("failed to get current directory: %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
}
secret.Debug("resolveVaultSymlink completed successfully", "result", target)
return target, nil
}
}
// 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) {
secret.Debug("Getting current vault", "state_dir", stateDir)
// Check if the current vault symlink exists
currentVaultPath := filepath.Join(stateDir, "currentvault")
secret.Debug("Checking current vault symlink", "path", currentVaultPath)
_, err := fs.Stat(currentVaultPath)
if err != nil {
secret.Debug("Failed to stat current vault symlink", "error", err, "path", currentVaultPath)
return nil, fmt.Errorf("failed to read current vault symlink: %w", err)
}
secret.Debug("Current vault symlink exists")
// Resolve the symlink to get the actual vault directory
secret.Debug("Resolving vault symlink")
targetPath, err := ResolveVaultSymlink(fs, currentVaultPath)
if err != nil {
return nil, err
}
secret.Debug("Resolved vault symlink", "target_path", targetPath)
// Extract the vault name from the path
// The path will be something like "/path/to/vaults.d/default"
vaultName := filepath.Base(targetPath)
secret.Debug("Extracted vault name", "vault_name", vaultName)
secret.Debug("Current vault resolved", "vault_name", vaultName, "target_path", targetPath)
// Create and return the vault
return NewVault(fs, stateDir, vaultName), nil
}
// ListVaults lists all vaults in the state directory
func ListVaults(fs afero.Fs, stateDir string) ([]string, error) {
vaultsDir := filepath.Join(stateDir, "vaults.d")
// Check if vaults directory exists
exists, err := afero.DirExists(fs, vaultsDir)
if err != nil {
return nil, fmt.Errorf("failed to check if vaults directory exists: %w", err)
}
if !exists {
return []string{}, nil
}
// Read the vaults directory
entries, err := afero.ReadDir(fs, vaultsDir)
if err != nil {
return nil, fmt.Errorf("failed to read vaults directory: %w", err)
}
// Extract vault names
var vaults []string
for _, entry := range entries {
if entry.IsDir() {
vaults = append(vaults, entry.Name())
}
}
return vaults, nil
}
// CreateVault creates a new vault
func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
secret.Debug("Creating new vault", "name", name, "state_dir", stateDir)
// Validate vault name
if !isValidVaultName(name) {
secret.Debug("Invalid vault name provided", "vault_name", name)
return nil, fmt.Errorf("invalid vault name '%s': must match pattern [a-z0-9.\\-_]+", name)
}
secret.Debug("Vault name validation passed", "vault_name", name)
// Create vault directory structure
vaultDir := filepath.Join(stateDir, "vaults.d", name)
secret.Debug("Creating vault directory structure", "vault_dir", vaultDir)
// Create main vault directory
if err := fs.MkdirAll(vaultDir, secret.DirPerms); err != nil {
return nil, fmt.Errorf("failed to create vault directory: %w", err)
}
// Create secrets directory
secretsDir := filepath.Join(vaultDir, "secrets.d")
if err := fs.MkdirAll(secretsDir, secret.DirPerms); err != nil {
return nil, fmt.Errorf("failed to create secrets directory: %w", err)
}
// Create unlockers directory
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
if err := fs.MkdirAll(unlockersDir, secret.DirPerms); err != nil {
return nil, fmt.Errorf("failed to create unlockers directory: %w", err)
}
// Save initial vault metadata (without derivation info until a mnemonic is imported)
metadata := &VaultMetadata{
Name: name,
CreatedAt: time.Now(),
DerivationIndex: 0,
LongTermKeyHash: "", // Will be set when mnemonic is imported
MnemonicHash: "", // Will be set when mnemonic is imported
}
if err := SaveVaultMetadata(fs, vaultDir, metadata); err != nil {
return nil, fmt.Errorf("failed to save vault metadata: %w", err)
}
// Select the newly created vault as current
secret.Debug("Selecting newly created vault as current", "name", name)
if err := SelectVault(fs, stateDir, name); err != nil {
return nil, fmt.Errorf("failed to select vault: %w", err)
}
// Create and return the vault
secret.Debug("Successfully created vault", "name", name)
return NewVault(fs, stateDir, name), nil
}
// SelectVault selects the given vault as the current vault
func SelectVault(fs afero.Fs, stateDir string, name string) error {
secret.Debug("Selecting vault", "vault_name", name, "state_dir", stateDir)
// Validate vault name
if !isValidVaultName(name) {
secret.Debug("Invalid vault name provided", "vault_name", name)
return fmt.Errorf("invalid vault name '%s': must match pattern [a-z0-9.\\-_]+", name)
}
secret.Debug("Vault name validation passed", "vault_name", name)
// Check if vault exists
vaultDir := filepath.Join(stateDir, "vaults.d", name)
exists, err := afero.DirExists(fs, vaultDir)
if err != nil {
return fmt.Errorf("failed to check if vault exists: %w", err)
}
if !exists {
return fmt.Errorf("vault %s does not exist", name)
}
// Create or update the current vault symlink/file
currentVaultPath := filepath.Join(stateDir, "currentvault")
targetPath := filepath.Join(stateDir, "vaults.d", name)
// First try to remove existing symlink if it exists
if _, err := fs.Stat(currentVaultPath); err == nil {
secret.Debug("Removing existing current vault symlink", "path", currentVaultPath)
// Ignore errors from Remove as we'll try to create/update it anyway.
// On some systems, removing a symlink may fail but the subsequent create may still succeed.
_ = fs.Remove(currentVaultPath)
}
// Try to create a real symlink first (works on Unix systems)
if _, ok := fs.(*afero.OsFs); ok {
secret.Debug("Creating vault symlink", "target", targetPath, "link", currentVaultPath)
if err := os.Symlink(targetPath, currentVaultPath); err == nil {
secret.Debug("Successfully selected vault", "vault_name", name)
return nil
}
// If symlink creation fails, fall back to regular file
}
// Fallback: create a regular file with the target path
secret.Debug("Fallback: creating regular file with target path", "target", targetPath)
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)
return nil
}