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 unlock keys directory unlockKeysDir := filepath.Join(vaultDir, "unlock.d") if err := fs.MkdirAll(unlockKeysDir, secret.DirPerms); err != nil { return nil, fmt.Errorf("failed to create unlock keys 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 }