Name only the mnemonic when it cannot be read (closes #115)
check / check (push) Failing after 2s
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:
+37
-18
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user