diff --git a/CLAUDE.md b/CLAUDE.md index c1a0793..c83612f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,6 +37,7 @@ go test ./auth/ -run TestCacheExpiry | `web` | Session cookies, auth middleware, template rendering for htmx UIs | | `health` | REST and gRPC health check handlers | | `archive` | tar.zst snapshots with SQLite-aware backup (VACUUM INTO) | +| `terminal` | Secure terminal input (echo-suppressed password prompts) | ## Import Pattern @@ -63,6 +64,7 @@ db --> (modernc.org/sqlite) grpcserver --> auth, config health --> db httpserver --> config +terminal --> (golang.org/x/term) web --> auth, csrf ``` diff --git a/go.mod b/go.mod index 2b30748..f3964ac 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/protobuf v1.36.10 // indirect diff --git a/go.sum b/go.sum index 75ea248..e96a060 100644 --- a/go.sum +++ b/go.sum @@ -49,6 +49,8 @@ golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= diff --git a/terminal/terminal.go b/terminal/terminal.go new file mode 100644 index 0000000..3269794 --- /dev/null +++ b/terminal/terminal.go @@ -0,0 +1,22 @@ +// Package terminal provides secure terminal input helpers for CLI tools. +package terminal + +import ( + "fmt" + "os" + + "golang.org/x/term" +) + +// ReadPassword prints the given prompt to stderr and reads a password +// from the terminal with echo disabled. It prints a newline after the +// input is complete so the cursor advances normally. +func ReadPassword(prompt string) (string, error) { + fmt.Fprint(os.Stderr, prompt) + b, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec // fd fits in int + fmt.Fprintln(os.Stderr) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/terminal/terminal_test.go b/terminal/terminal_test.go new file mode 100644 index 0000000..ba3074f --- /dev/null +++ b/terminal/terminal_test.go @@ -0,0 +1,14 @@ +package terminal + +import ( + "testing" +) + +func TestReadPasswordNotATTY(t *testing.T) { + // When stdin is not a terminal (e.g. in CI), ReadPassword should + // return an error rather than hanging or panicking. + _, err := ReadPassword("Password: ") + if err == nil { + t.Fatal("expected error when stdin is not a terminal") + } +}