diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-22 17:19:23 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-22 17:19:23 +0200 |
| commit | 2184c2a33b9c00a21d8816f42f5b76d5b9d59be6 (patch) | |
| tree | ac7ff9f0c1fdfd1d4da68e7a02b07a26f0a81851 /internal | |
| parent | 8b627f383e68ce2b71832d26e86f621239271ad0 (diff) | |
Fix PIN prompt and Ctrl+C behaviour in shell
PIN prompt: replace readline.ReadPassword (which silently failed to
display the prompt before the process was fully interactive) with
golang.org/x/term.ReadPassword, which reliably disables echo and prints
the prompt via a plain fmt.Print before reading. This fixes the root cause
of the decryption failures — the user was never prompted for their PIN,
so an empty/default PIN was used, producing a wrong IV.
Ctrl+C: return io.EOF from Shell.ReadLine on readline.ErrInterrupt so
that pressing Ctrl+C exits the shell loop, matching the Ruby behaviour
where SIGINT terminates the process.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/shell/shell.go | 36 |
1 files changed, 16 insertions, 20 deletions
diff --git a/internal/shell/shell.go b/internal/shell/shell.go index 5395c97..1d231b9 100644 --- a/internal/shell/shell.go +++ b/internal/shell/shell.go @@ -6,10 +6,13 @@ package shell import ( "context" + "fmt" "io" + "os" "strings" "github.com/ergochat/readline" + "golang.org/x/term" ) // Shell manages an interactive readline loop with vi mode and tab completion. @@ -83,9 +86,9 @@ func New(completionFn func(prefix string) []string) (*Shell, error) { // ReadLine reads one line from the terminal, applying history deduplication. // -// Behaviour mirrors the Ruby shell_loop: +// Behaviour: // - Ctrl+D (EOF) → returns ("", io.EOF) — caller should exit -// - Ctrl+C (interrupt) → returns ("", nil) — empty line, continue +// - Ctrl+C (interrupt) → returns ("", io.EOF) — caller should exit (same as Ruby's SIGINT) // - non-empty line → saved to history only if it differs from the // previous entry, then returned to the caller // @@ -99,8 +102,9 @@ func (s *Shell) ReadLine(ctx context.Context) (string, error) { return "", io.EOF } if err == readline.ErrInterrupt { - // Ctrl+C — return an empty line so the caller loops again. - return "", nil + // Ctrl+C — exit the shell, mirroring the Ruby behaviour where + // SIGINT terminates the process. + return "", io.EOF } return "", err } @@ -137,25 +141,17 @@ func (s *Shell) ReadPassword(prompt string) (string, error) { return string(bytes), nil } -// ReadPassword reads a password from stdin without echoing characters. -// It is a package-level convenience function for use before the Shell is -// created, such as during initial PIN entry at startup. +// ReadPassword prints prompt then reads a password from the terminal without +// echoing characters. It uses golang.org/x/term for reliable cross-platform +// masked input, bypassing the readline library which does not always display +// the prompt correctly before the process is fully interactive. func ReadPassword(prompt string) (string, error) { - // Create a minimal, temporary readline instance solely for masked input. - // VimMode and history are irrelevant here; we just need the password- - // reading capability. - rl, err := readline.NewFromConfig(&readline.Config{ - Prompt: prompt, - HistoryLimit: -1, // disable history for this throwaway instance - }) - if err != nil { - return "", err - } - defer rl.Close() //nolint:errcheck + fmt.Print(prompt) + defer fmt.Println() // move to next line after the user presses Enter - bytes, err := rl.ReadPassword(prompt) + b, err := term.ReadPassword(int(os.Stdin.Fd())) if err != nil { return "", err } - return string(bytes), nil + return string(b), nil } |
