Ask for the passphrase on the controlling terminal, not stdin (closes #144)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The passphrase and mnemonic prompts open /dev/tty and ask there, so `echo v | secret add s1 --force`, `secret encrypt` and `secret decrypt` with piped input ask at a terminal instead of failing. Only a process with no controlling terminal still gets the error naming the variable to set. `secret init` and `secret vault create` no longer write a newline to stderr after the mnemonic. On SIGINT or SIGTERM, Entry() restores the settings of /dev/tty instead of stdin, since that is where the prompt turns echo off. The two tests that pin the no-terminal message run the binary in a new session, so it has no terminal wherever the tests run. Unverified by a test: the echo restore after Ctrl-C, checked by hand. Model: opus-5-5
This commit was merged in pull request #165.
This commit is contained in:
@@ -17,6 +17,10 @@ const (
|
||||
// EnvGPGKeyID is the environment variable for providing the GPG key ID
|
||||
EnvGPGKeyID = "SB_GPG_KEY_ID"
|
||||
|
||||
// TerminalPath opens the controlling terminal of the process that opens
|
||||
// it, where the passphrase and mnemonic prompts ask.
|
||||
TerminalPath = "/dev/tty"
|
||||
|
||||
// UnlockerTimeFormat is the layout of the time, in UTC, in the name of a
|
||||
// new unlocker's directory. It runs to the nanosecond, so that every new
|
||||
// unlocker, even one added right after another, gets a directory of its
|
||||
|
||||
+17
-26
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"filippo.io/age"
|
||||
@@ -16,11 +15,8 @@ import (
|
||||
|
||||
var (
|
||||
errNilPassphraseBuffer = errors.New("passphrase buffer is nil")
|
||||
errStdinNotTerminal = errors.New(
|
||||
"stdin is not a terminal (piped input or script)")
|
||||
errStderrNotTerminal = errors.New(
|
||||
"stderr is not a terminal (running in non-interactive mode)")
|
||||
errNothingEntered = errors.New("nothing was entered")
|
||||
errNoTerminal = errors.New("there is no terminal to ask on")
|
||||
errNothingEntered = errors.New("nothing was entered")
|
||||
)
|
||||
|
||||
// ErrMnemonicNotRead is wrapped in every error of ReadMnemonic: there is no
|
||||
@@ -193,38 +189,33 @@ func ReadMnemonic(prompt string) (*memguard.LockedBuffer, error) {
|
||||
return readFromTerminal(prompt, ErrMnemonicNotRead, EnvMnemonic)
|
||||
}
|
||||
|
||||
// readFromTerminal reads input from the terminal without echoing it. Every
|
||||
// error it returns wraps notRead; without a terminal, the error says to set
|
||||
// envVar instead.
|
||||
// readFromTerminal asks on the controlling terminal and reads the answer
|
||||
// there without echoing it, whatever stdin is: in
|
||||
// `echo value | secret add name --force`, stdin is a pipe. Every error it
|
||||
// returns wraps notRead; without a terminal, the error says to set envVar
|
||||
// instead.
|
||||
func readFromTerminal(
|
||||
prompt string, notRead error, envVar string,
|
||||
) (*memguard.LockedBuffer, error) {
|
||||
// Check if stdin is a terminal
|
||||
if !term.IsTerminal(syscall.Stdin) {
|
||||
// Not a terminal - never read secrets from piped input
|
||||
// for security reasons
|
||||
terminal, err := os.OpenFile(TerminalPath, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
Debug("Failed to open the controlling terminal", "error", err)
|
||||
|
||||
return nil, fmt.Errorf(
|
||||
"%w: %w. Please set the %s environment variable or run interactively",
|
||||
notRead, errStdinNotTerminal, envVar)
|
||||
notRead, errNoTerminal, envVar)
|
||||
}
|
||||
defer func() { _ = terminal.Close() }()
|
||||
|
||||
// stdin is a terminal, check if stderr is also a terminal for
|
||||
// interactive prompting
|
||||
if !term.IsTerminal(syscall.Stderr) {
|
||||
return nil, fmt.Errorf("%w: %w. Please set the %s environment variable",
|
||||
notRead, errStderrNotTerminal, envVar)
|
||||
}
|
||||
_, _ = fmt.Fprint(terminal, prompt)
|
||||
|
||||
// Both stdin and stderr are terminals - use secure password reading
|
||||
fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
|
||||
|
||||
input, err := term.ReadPassword(syscall.Stdin)
|
||||
input, err := term.ReadPassword(int(terminal.Fd()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", notRead, err)
|
||||
}
|
||||
|
||||
// Print newline to stderr since ReadPassword doesn't echo
|
||||
fmt.Fprintln(os.Stderr)
|
||||
// The newline typed after the input, which ReadPassword did not echo
|
||||
_, _ = fmt.Fprintln(terminal)
|
||||
|
||||
if len(input) == 0 {
|
||||
return nil, fmt.Errorf("%w: %w", notRead, errNothingEntered)
|
||||
|
||||
Reference in New Issue
Block a user