Name only the mnemonic when it cannot be read (closes #115)
check / check (push) Failing after 2s

secret init and secret vault create read the mnemonic with the new
secret.ReadMnemonic, whose every error wraps the new
secret.ErrMnemonicNotRead. It shares the terminal read with ReadPassphrase,
whose errors still wrap ErrPassphraseNotRead. Without a terminal the error
names the environment variable that gives the value instead:
SB_SECRET_MNEMONIC for the mnemonic, SB_UNLOCK_PASSPHRASE for the
passphrase. A test pins the message of init without a terminal.

Model: opus-5-5
This commit was merged in pull request #116.
This commit is contained in:
2026-10-05 01:43:00 +02:00
parent 43f66bf369
commit 2503f2db96
5 changed files with 86 additions and 23 deletions
+37 -18
View File
@@ -17,16 +17,17 @@ import (
var (
errNilPassphraseBuffer = errors.New("passphrase buffer is nil")
errStdinNotTerminal = errors.New(
"cannot read passphrase from non-terminal stdin " +
"(piped input or script). Please set the SB_UNLOCK_PASSPHRASE " +
"environment variable or run interactively")
"stdin is not a terminal (piped input or script)")
errStderrNotTerminal = errors.New(
"cannot prompt for passphrase: stderr is not a terminal " +
"(running in non-interactive mode). Please set the " +
"SB_UNLOCK_PASSPHRASE environment variable")
"stderr is not a terminal (running in non-interactive mode)")
errNothingEntered = errors.New("nothing was entered")
errEmptyPassphrase = errors.New("passphrase cannot be empty")
)
// ErrMnemonicNotRead is wrapped in every error of ReadMnemonic: there is no
// terminal to read the mnemonic from, reading it failed, or it was empty.
var ErrMnemonicNotRead = errors.New("failed to read mnemonic")
// EncryptToRecipient encrypts data to a recipient using age
// The data parameter should be a LockedBuffer for secure memory handling
func EncryptToRecipient(
@@ -169,40 +170,58 @@ func DecryptWithPassphrase(
// Returns a LockedBuffer containing the passphrase for secure memory handling.
// Every error it returns wraps ErrPassphraseNotRead.
func ReadPassphrase(prompt string) (*memguard.LockedBuffer, error) {
return readFromTerminal(prompt, ErrPassphraseNotRead, EnvUnlockPassphrase)
}
// ReadMnemonic reads a mnemonic from the terminal as ReadPassphrase reads a
// passphrase. Every error it returns wraps ErrMnemonicNotRead.
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.
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 passphrases from piped input
// Not a terminal - never read secrets from piped input
// for security reasons
return nil, fmt.Errorf("%w: %w", ErrPassphraseNotRead, errStdinNotTerminal)
return nil, fmt.Errorf(
"%w: %w. Please set the %s environment variable or run interactively",
notRead, errStdinNotTerminal, envVar)
}
// 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", ErrPassphraseNotRead, errStderrNotTerminal)
return nil, fmt.Errorf("%w: %w. Please set the %s environment variable",
notRead, errStderrNotTerminal, envVar)
}
// Both stdin and stderr are terminals - use secure password reading
fmt.Fprint(os.Stderr, prompt) // Write prompt to stderr, not stdout
passphrase, err := term.ReadPassword(syscall.Stdin)
input, err := term.ReadPassword(syscall.Stdin)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrPassphraseNotRead, err)
return nil, fmt.Errorf("%w: %w", notRead, err)
}
// Print newline to stderr since ReadPassword doesn't echo
fmt.Fprintln(os.Stderr)
if len(passphrase) == 0 {
return nil, fmt.Errorf("%w: %w", ErrPassphraseNotRead, errEmptyPassphrase)
if len(input) == 0 {
return nil, fmt.Errorf("%w: %w", notRead, errNothingEntered)
}
// Create a secure buffer and copy the passphrase
secureBuffer := memguard.NewBufferFromBytes(passphrase)
// Create a secure buffer and copy the input
secureBuffer := memguard.NewBufferFromBytes(input)
// Clear the original passphrase slice
for i := range passphrase {
passphrase[i] = 0
// Clear the original input slice
for i := range input {
input[i] = 0
}
return secureBuffer, nil