v3.3.1 31f1eec

pass.go RawHistoryFolder

1// Package secrets implements secret provider adapters.
2package secrets
3
4import (
5	"context"
6	"fmt"
7	"os/exec"
8	"regexp"
9	"strings"
10	"time"
11
12	"github.com/bnema/zerowrap"
13
14	"github.com/bnema/gordon/internal/domain"
15	"github.com/bnema/gordon/pkg/childenv"
16)
17
18// passPathRegex validates pass paths.
19// Allows alphanumeric characters, forward slashes, underscores, dots, and hyphens.
20// This prevents command injection via shell metacharacters.
21var passPathRegex = regexp.MustCompile(`^[a-zA-Z0-9/_.-]+$`)
22
23// PassProvider implements the SecretProvider interface using the pass password manager.
24type PassProvider struct {
25	timeout time.Duration
26	log     zerowrap.Logger
27}
28
29// NewPassProvider creates a new pass provider.
30func NewPassProvider(log zerowrap.Logger) *PassProvider {
31	return &PassProvider{
32		timeout: 10 * time.Second,
33		log:     log,
34	}
35}
36
37// Name returns the provider name.
38func (p *PassProvider) Name() string {
39	return "pass"
40}
41
42// ValidatePath validates a pass path to prevent command injection and path traversal.
43// Returns an error if the path is invalid.
44func ValidatePath(path string) error {
45	if path == "" {
46		return fmt.Errorf("path cannot be empty")
47	}
48
49	// SECURITY: Validate path format to prevent command injection
50	if !passPathRegex.MatchString(path) {
51		return fmt.Errorf("invalid path: must contain only alphanumeric characters, /, _, ., -")
52	}
53
54	// SECURITY: Prevent path traversal
55	if strings.Contains(path, "..") {
56		return domain.ErrPathTraversal
57	}
58
59	// SECURITY: Reject absolute paths
60	if strings.HasPrefix(path, "/") {
61		return fmt.Errorf("invalid path: absolute paths not allowed")
62	}
63
64	return nil
65}
66
67// GetSecret retrieves a secret from pass by path.
68func (p *PassProvider) GetSecret(ctx context.Context, path string) (string, error) {
69	// SECURITY: Validate path before executing command
70	if err := ValidatePath(path); err != nil {
71		p.log.Warn().
72			Str(zerowrap.FieldLayer, "adapter").
73			Str(zerowrap.FieldAdapter, "secrets").
74			Str("provider", "pass").
75			Str("path", path).
76			Err(err).
77			Msg("rejected invalid pass path")
78		return "", err
79	}
80
81	ctx, cancel := context.WithTimeout(ctx, p.timeout)
82	defer cancel()
83
84	cmd := childenv.CommandContext(ctx, "pass", "show", path)
85	output, err := cmd.Output()
86	if err != nil {
87		if exitError, ok := err.(*exec.ExitError); ok {
88			return "", fmt.Errorf("pass command failed: %s", string(exitError.Stderr))
89		}
90		return "", fmt.Errorf("failed to execute pass command: %w", err)
91	}
92
93	secret := strings.TrimSpace(string(output))
94	if secret == "" {
95		return "", fmt.Errorf("empty secret returned from pass for path: %s", path)
96	}
97
98	p.log.Debug().
99		Str(zerowrap.FieldLayer, "adapter").
100		Str(zerowrap.FieldAdapter, "secrets").
101		Str("provider", "pass").
102		Str("path", path).
103		Msg("successfully retrieved secret from pass")
104
105	return secret, nil
106}
107
108// IsAvailable checks if pass is available in the system.
109// Uses a short timeout so a stalled GPG agent does not hang the caller.
110func (p *PassProvider) IsAvailable() bool {
111	ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
112	defer cancel()
113	cmd := childenv.CommandContext(ctx, "pass", "version")
114	return cmd.Run() == nil
115}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close