v0.3.2 a514042

source_linux.go RawHistoryFolder

1package auth
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"os"
8	"os/exec"
9	"strings"
10	"syscall"
11	"time"
12
13	"github.com/bnema/neferafk/internal/ports"
14)
15
16const sourceLimit = 4096
17
18var errSource = errors.New("PIN source unavailable")
19
20func validPIN(b []byte) bool {
21	if len(b) < 6 || len(b) > 32 {
22		return false
23	}
24	for _, v := range b {
25		if v < '0' || v > '9' {
26			return false
27		}
28	}
29	return true
30}
31
32func passEntry(entry string) bool {
33	if entry == "" || len(entry) > ports.AuthMaxMetadata || strings.HasPrefix(entry, "-") || strings.HasPrefix(entry, "/") || strings.ContainsAny(entry, "\\\x00\r\n") {
34		return false
35	}
36	for _, part := range strings.Split(entry, "/") {
37		if part == "" || part == "." || part == ".." {
38			return false
39		}
40	}
41	return true
42}
43
44// fixedOutput bounds stdout without retaining raw errors or growing buffers.
45type fixedOutput struct {
46	data [sourceLimit]byte
47	n    int
48}
49
50func (b *fixedOutput) Write(p []byte) (int, error) {
51	if len(p) > len(b.data)-b.n {
52		return 0, errSource
53	}
54	copy(b.data[b.n:], p)
55	b.n += len(p)
56	return len(p), nil
57}
58
59func passEnvironment() []string {
60	var env []string
61	for _, key := range []string{"HOME", "PATH", "GNUPGHOME", "PASSWORD_STORE_DIR"} {
62		if v, ok := os.LookupEnv(key); ok {
63			env = append(env, key+"="+v)
64		}
65	}
66	// Installed pass uses PASSWORD_STORE_GPG_OPTS; prevent TTY/pinentry and
67	// extension execution. No startup PIN or typed secret enters this env.
68	return append(env, "PASSWORD_STORE_GPG_OPTS=--batch --no-tty --pinentry-mode error", "PASSWORD_STORE_ENABLE_EXTENSIONS=false", "GPG_TTY=", "LC_ALL=C")
69}
70
71func readPass(ctx context.Context, entry string) ([]byte, error) {
72	if !passEntry(entry) {
73		return nil, errSource
74	}
75	return commandPIN(ctx, "pass", []string{"show", entry}, passEnvironment())
76}
77
78// commandPIN owns only the bounded source subprocess, never a shell command
79// assembled from metadata. Tests use a generic child, not a real pass store.
80func commandPIN(ctx context.Context, executable string, args, env []string) ([]byte, error) {
81	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
82	defer cancel()
83	cmd := exec.CommandContext(ctx, executable, args...)
84	cmd.Env = env
85	cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
86	// Kill the complete source group, then Run's Wait reaps the child.
87	cmd.Cancel = func() error { return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) }
88	cmd.WaitDelay = 100 * time.Millisecond
89	var out fixedOutput
90	cmd.Stdout = &out
91	defer clear(out.data[:])
92	if err := cmd.Run(); err != nil {
93		return nil, errSource
94	}
95	line := out.data[:out.n]
96	if i := bytes.IndexByte(line, '\n'); i >= 0 {
97		line = line[:i]
98	}
99	if !validPIN(line) {
100		return nil, errSource
101	}
102	return bytes.Clone(line), nil
103}
104
105// ResolvePIN is called once per lock, not once per attempt. The caller owns and
106// must wipe the returned mutable slice. Invalid/unavailable source means PAM.
107func ResolvePIN(ctx context.Context, bootstrap ports.AuthBootstrap) []byte {
108	switch bootstrap.PINSource {
109	case ports.PINSourceEnv:
110		if validPIN(bootstrap.EnvPIN) {
111			return bytes.Clone(bootstrap.EnvPIN)
112		}
113	case ports.PINSourcePass:
114		pin, err := readPass(ctx, bootstrap.PINReference)
115		if err == nil {
116			return pin
117		}
118	}
119	return nil
120}

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 2

Tags 9

↑↓ moveenter openesc close